diff --git a/Client/udpscope/FrameDecoder.cpp b/Client/udpscope/FrameDecoder.cpp index 241b022..c74f31f 100644 --- a/Client/udpscope/FrameDecoder.cpp +++ b/Client/udpscope/FrameDecoder.cpp @@ -328,15 +328,25 @@ bool FrameDecoder::timestamps(const FrameView& f, uint32_t idx, if (factor < kMinBleedFactor) { factor = kMinBleedFactor; } double advance = nominal * factor; + /* A non-positive elapsed means the wall has not moved + * since this signal's previous burst — a coarse arrival + * clock, or two packets stamped within one tick of it. + * There is no wall time to spend, so the cap is zero. + * Skipping the cap in that case (which is what this code + * used to do) hands back the full proportional advance, + * so a run of same-tick arrivals gains lead while no wall + * time passes at all — the divergence the cap exists to + * stop, in its purest form. */ const double wallElapsed = wallNow - st.lastEmittedWall; - if (wallElapsed > 0.0) { - const double cap = kWallBleedFraction * wallElapsed; - if (cap < advance) { advance = cap; } - } + const double cap = (wallElapsed > 0.0) + ? (kWallBleedFraction * wallElapsed) + : 0.0; + if (cap < advance) { advance = cap; } step = advance / static_cast(nElems); - /* Unreachable with a finite positive dt — kept because - * downstream monotonicity must not depend on that - * argument holding for every value off the wire. */ + /* Reached whenever the cap is zero, and a backstop + * against a nonsensical dt off the wire: downstream + * requires strictly increasing stamps, so the burst must + * still advance by something. */ if (!(step > 0.0)) { step = dt * kMinBleedFactor; } base = st.lastEmittedEnd + step; } @@ -356,7 +366,37 @@ bool FrameDecoder::timestamps(const FrameView& f, uint32_t idx, /* No declared rate: need hrt-derived dt. */ if (!hrtFit_.ready() || f.hrt == 0u) { - return packetBurst(idx, nElems, wallNow, tsOut); + const bool ok = packetBurst(idx, nElems, wallNow, tsOut); + /* Carry the warm-up's state into the hrt branch, or the handover + * from one to the other is a discontinuity in both directions. + * + * The producer-clock reference (lastAccHrt, prevAccCount) matters + * most. Without it the first hrt packet has no previous tick to + * subtract, falls back to kDefaultDt for its inter-element step and + * latches ClockOffset against wallNow - (nElems-1)*kDefaultDt. + * kDefaultDt is only right when the burst happens to run at 1 kHz; + * at 100 samples per 10 ms packet it is ten times too wide and the + * latch lands 89 ms in the past — permanently, since it is below + * ClockOffset's recalibration threshold. Seeding here means the + * first hrt packet measures a real tick delta and latches correctly. + * + * The emitted-timeline reference (lastEmitted*) then only has to + * cover residual disagreement, but it is what keeps the handover + * MONOTONIC: packetBurst ends its burst at wallNow while the hrt + * branch ends at wallNow - (nElems-1)*hrtDt, and without a previous + * end to clamp against the first hrt packet steps the signal + * backwards by up to a whole burst width. */ + if (f.hrt != 0u) { + st.lastAccHrt = f.hrt; + st.lastAccValid = true; + } + st.prevAccCount = nElems; + if (!ok) { return false; } + st.lastEmittedEnd = tsOut[nElems - 1u]; + st.lastEmittedWall = wallNow; + st.lastCounter = f.counter; + st.lastEmittedValid = true; + return true; } const double rate = hrtFit_.ticksPerSecond(); @@ -455,11 +495,16 @@ bool FrameDecoder::timestamps(const FrameView& f, uint32_t idx, * at kWallBleedFraction: only that makes the lead bleed off. */ if (st.lastEmittedValid && base <= st.lastEmittedEnd) { const double wallElapsed = wallNow - st.lastEmittedWall; - if (wallElapsed > 0.0) { - const double cap = kWallBleedFraction * wallElapsed / - static_cast(nElems); - if (cap < step) { step = cap; } - } + /* No wall movement, no wall time to spend: see the same cap in the + * declared branch. Zero rather than "skip the cap", so a run of + * same-tick arrivals cannot advance a full hrtDt per sample while + * the wall stands still. */ + const double cap = (wallElapsed > 0.0) + ? (kWallBleedFraction * wallElapsed / + static_cast(nElems)) + : 0.0; + if (cap < step) { step = cap; } + if (!(step > 0.0)) { step = hrtDt * kMinBleedFactor; } base = st.lastEmittedEnd + step; } diff --git a/Client/udpscope/FrameDecoder.h b/Client/udpscope/FrameDecoder.h index 7f6f2d9..4795fd2 100644 --- a/Client/udpscope/FrameDecoder.h +++ b/Client/udpscope/FrameDecoder.h @@ -10,7 +10,7 @@ * Source/Applications/StreamHub/UDPSourceSession.cpp documents this failure and * solves it; these are the same rules, computed from udps_frame_t's own fields. * - * Two rules deliberately differ, both in the accumulated-scalar case (rule 3). + * Three rules deliberately differ, all in the accumulated-scalar case (rule 3). * * First, the anchor. StreamHub anchors every accumulated-scalar burst on the * packet's own hrt, converted with the LOCAL MARTe HighResolutionTimer @@ -24,7 +24,15 @@ * reconstructed timeline drift, and drift that only arrival time can observe * must be corrected against arrival time — see rule 3. * - * Second, the entry condition. UDPSourceSession.cpp:554 routes any update + * Second, which end of the burst is anchored. StreamHub converts the packet's + * hrt into the position of sample 0 and steps forward, so the burst STARTS at + * the anchor. Here the anchor is arrival time, and the samples were acquired + * before the packet carrying them landed — so the burst must END there instead. + * Both branches of rule 3 do this, or two accumulated scalars in one scope, one + * with a declared rate and one without, would sit a whole burst apart on the + * shared X axis. + * + * Third, the entry condition. UDPSourceSession.cpp:554 routes any update * carrying nElems <= 1 to plain arrival time. That is safe for a host-local * consumer whose arrival time is the producer's own clock, but wrong here: * Accumulate mode flushes on a TIMER, so a short RT cycle legitimately delivers diff --git a/Client/udpscope/tests/FrameDecoderTest.cpp b/Client/udpscope/tests/FrameDecoderTest.cpp index 078e9ba..96a2cb0 100644 --- a/Client/udpscope/tests/FrameDecoderTest.cpp +++ b/Client/udpscope/tests/FrameDecoderTest.cpp @@ -429,7 +429,14 @@ TEST(FrameDecoder, AccumulatedScalarWithANonFiniteRateFallsBackToTheHrtPath) { } ASSERT_EQ(last.size(), 10u); - EXPECT_NEAR(last[1] - last[0], 0.0025, 2e-5) + /* The tolerance is bounded from both sides and neither bound is arbitrary. + * Below: hrtDt divides a tick delta by HrtRateFit's fitted rate, and the fit + * regresses hrt against arrivals carrying the +/-3 ms jitter above, so ~2 us + * of residual is inherent — 1e-8 fails. Above: the degenerate declared branch + * would span those same jittered gaps and answer 2.2 or 2.8 ms, 300 us out. + * 1e-5 sits two orders below the thing it must reject and five times above + * the noise it must tolerate. */ + EXPECT_NEAR(last[1] - last[0], 0.0025, 1e-5) << "an unusable declared rate must fall through to the hrt path"; } @@ -811,6 +818,71 @@ TEST(FrameDecoder, UndeclaredAccumulatedScalarEndsItsBurstOnArrival) { EXPECT_NEAR(last[0], lastArrival - 0.009, 1e-9); } +// An undeclared-rate signal is served by TWO different mechanisms in sequence: +// packetBurst spans arrival gaps until HrtRateFit has collected enough packets, +// then the hrt branch takes over. They place a burst differently — packetBurst +// ends it at wallNow, the hrt branch at wallNow - (nElems-1)*hrtDt — so the +// handover is where a discontinuity hides. It is invisible at 10 samples per +// 10 ms packet, the one cadence where the derived period equals the kDefaultDt +// fallback, which is exactly why the other tests here could not see it. Sweep +// cadences either side of that coincidence. +TEST(FrameDecoder, UndeclaredAccumulatedScalarCrossesTheHrtHandoverCleanly) { + struct Case { uint32_t nElems; double packetSec; }; + const Case cases[] = { + {10u, 0.0025}, /* 4 kHz: burst wider than the packet interval */ + {100u, 0.010 }, /* 10 kHz */ + {1000u, 0.010 }, /* 100 kHz: a burst is 100x the kDefaultDt guess */ + {10u, 0.050 }, /* 200 Hz: burst narrower than the packet interval */ + }; + + for (const Case& c : cases) { + FrameDecoder dec; + dec.setSignals({undeclaredAcc()}); + + const double ticks = 1.0e9; + const uint64_t bootHrt = static_cast(86400.0 * ticks); + const double sampleDt = c.packetSec / static_cast(c.nElems); + double last = 0.0; + bool seen = false; + double lastArrival = 0.0; + std::vector lastTs; + + for (int p = 0; p < 200; p++) { + FrameBuilder fb; + fb.addSignal(std::vector(c.nElems, 1.0)); + const uint64_t hrt = + bootHrt + static_cast(p * c.packetSec * ticks); + const double arrival = 700.0 + p * c.packetSec; + const FrameView& f = fb.build(hrt, arrival, c.nElems, + static_cast(p + 1)); + dec.beginFrame(f); + std::vector ts; + if (!dec.timestamps(f, 0, ts)) { continue; } + for (double t : ts) { + if (seen) { + ASSERT_GT(t, last) + << "handover stepped back " << (last - t) << " s with " + << c.nElems << " samples per " << c.packetSec << " s packet"; + } + last = t; + seen = true; + } + lastTs = ts; + lastArrival = arrival; + } + + /* Monotonic is necessary but not sufficient: a clamp restores ordering + * while leaving the whole trace parked in the past. The producer clock + * here is exact, so once settled the burst must still end on arrival and + * step at the true sample period. */ + ASSERT_EQ(lastTs.size(), c.nElems); + EXPECT_NEAR(lastTs.back(), lastArrival, 1e-6) + << "trace drifted off the wall clock with " << c.nElems + << " samples per " << c.packetSec << " s packet"; + EXPECT_NEAR(lastTs[1] - lastTs[0], sampleDt, sampleDt * 1e-3); + } +} + // The same double delivery that the declared branch guards against — a host // joined on two interfaces receives every unfragmented update twice — reaches an // undeclared-rate signal identically. The guard can only fire if this branch diff --git a/docs/superpowers/plans/2026-08-27-udpscope.md b/docs/superpowers/plans/2026-08-27-udpscope.md index 6173cce..def60ce 100644 --- a/docs/superpowers/plans/2026-08-27-udpscope.md +++ b/docs/superpowers/plans/2026-08-27-udpscope.md @@ -1861,7 +1861,14 @@ TEST(FrameDecoder, AccumulatedScalarWithANonFiniteRateFallsBackToTheHrtPath) { } ASSERT_EQ(last.size(), 10u); - EXPECT_NEAR(last[1] - last[0], 0.0025, 2e-5) + /* The tolerance is bounded from both sides and neither bound is arbitrary. + * Below: hrtDt divides a tick delta by HrtRateFit's fitted rate, and the fit + * regresses hrt against arrivals carrying the +/-3 ms jitter above, so ~2 us + * of residual is inherent — 1e-8 fails. Above: the degenerate declared branch + * would span those same jittered gaps and answer 2.2 or 2.8 ms, 300 us out. + * 1e-5 sits two orders below the thing it must reject and five times above + * the noise it must tolerate. */ + EXPECT_NEAR(last[1] - last[0], 0.0025, 1e-5) << "an unusable declared rate must fall through to the hrt path"; } @@ -2243,6 +2250,71 @@ TEST(FrameDecoder, UndeclaredAccumulatedScalarEndsItsBurstOnArrival) { EXPECT_NEAR(last[0], lastArrival - 0.009, 1e-9); } +// An undeclared-rate signal is served by TWO different mechanisms in sequence: +// packetBurst spans arrival gaps until HrtRateFit has collected enough packets, +// then the hrt branch takes over. They place a burst differently — packetBurst +// ends it at wallNow, the hrt branch at wallNow - (nElems-1)*hrtDt — so the +// handover is where a discontinuity hides. It is invisible at 10 samples per +// 10 ms packet, the one cadence where the derived period equals the kDefaultDt +// fallback, which is exactly why the other tests here could not see it. Sweep +// cadences either side of that coincidence. +TEST(FrameDecoder, UndeclaredAccumulatedScalarCrossesTheHrtHandoverCleanly) { + struct Case { uint32_t nElems; double packetSec; }; + const Case cases[] = { + {10u, 0.0025}, /* 4 kHz: burst wider than the packet interval */ + {100u, 0.010 }, /* 10 kHz */ + {1000u, 0.010 }, /* 100 kHz: a burst is 100x the kDefaultDt guess */ + {10u, 0.050 }, /* 200 Hz: burst narrower than the packet interval */ + }; + + for (const Case& c : cases) { + FrameDecoder dec; + dec.setSignals({undeclaredAcc()}); + + const double ticks = 1.0e9; + const uint64_t bootHrt = static_cast(86400.0 * ticks); + const double sampleDt = c.packetSec / static_cast(c.nElems); + double last = 0.0; + bool seen = false; + double lastArrival = 0.0; + std::vector lastTs; + + for (int p = 0; p < 200; p++) { + FrameBuilder fb; + fb.addSignal(std::vector(c.nElems, 1.0)); + const uint64_t hrt = + bootHrt + static_cast(p * c.packetSec * ticks); + const double arrival = 700.0 + p * c.packetSec; + const FrameView& f = fb.build(hrt, arrival, c.nElems, + static_cast(p + 1)); + dec.beginFrame(f); + std::vector ts; + if (!dec.timestamps(f, 0, ts)) { continue; } + for (double t : ts) { + if (seen) { + ASSERT_GT(t, last) + << "handover stepped back " << (last - t) << " s with " + << c.nElems << " samples per " << c.packetSec << " s packet"; + } + last = t; + seen = true; + } + lastTs = ts; + lastArrival = arrival; + } + + /* Monotonic is necessary but not sufficient: a clamp restores ordering + * while leaving the whole trace parked in the past. The producer clock + * here is exact, so once settled the burst must still end on arrival and + * step at the true sample period. */ + ASSERT_EQ(lastTs.size(), c.nElems); + EXPECT_NEAR(lastTs.back(), lastArrival, 1e-6) + << "trace drifted off the wall clock with " << c.nElems + << " samples per " << c.packetSec << " s packet"; + EXPECT_NEAR(lastTs[1] - lastTs[0], sampleDt, sampleDt * 1e-3); + } +} + // The same double delivery that the declared branch guards against — a host // joined on two interfaces receives every unfragmented update twice — reaches an // undeclared-rate signal identically. The guard can only fire if this branch @@ -2383,7 +2455,7 @@ Create `Client/udpscope/FrameDecoder.h`: * Source/Applications/StreamHub/UDPSourceSession.cpp documents this failure and * solves it; these are the same rules, computed from udps_frame_t's own fields. * - * Two rules deliberately differ, both in the accumulated-scalar case (rule 3). + * Three rules deliberately differ, all in the accumulated-scalar case (rule 3). * * First, the anchor. StreamHub anchors every accumulated-scalar burst on the * packet's own hrt, converted with the LOCAL MARTe HighResolutionTimer @@ -2397,7 +2469,15 @@ Create `Client/udpscope/FrameDecoder.h`: * reconstructed timeline drift, and drift that only arrival time can observe * must be corrected against arrival time — see rule 3. * - * Second, the entry condition. UDPSourceSession.cpp:554 routes any update + * Second, which end of the burst is anchored. StreamHub converts the packet's + * hrt into the position of sample 0 and steps forward, so the burst STARTS at + * the anchor. Here the anchor is arrival time, and the samples were acquired + * before the packet carrying them landed — so the burst must END there instead. + * Both branches of rule 3 do this, or two accumulated scalars in one scope, one + * with a declared rate and one without, would sit a whole burst apart on the + * shared X axis. + * + * Third, the entry condition. UDPSourceSession.cpp:554 routes any update * carrying nElems <= 1 to plain arrival time. That is safe for a host-local * consumer whose arrival time is the producer's own clock, but wrong here: * Accumulate mode flushes on a TIMER, so a short RT cycle legitimately delivers @@ -2819,15 +2899,25 @@ bool FrameDecoder::timestamps(const FrameView& f, uint32_t idx, if (factor < kMinBleedFactor) { factor = kMinBleedFactor; } double advance = nominal * factor; + /* A non-positive elapsed means the wall has not moved + * since this signal's previous burst — a coarse arrival + * clock, or two packets stamped within one tick of it. + * There is no wall time to spend, so the cap is zero. + * Skipping the cap in that case (which is what this code + * used to do) hands back the full proportional advance, + * so a run of same-tick arrivals gains lead while no wall + * time passes at all — the divergence the cap exists to + * stop, in its purest form. */ const double wallElapsed = wallNow - st.lastEmittedWall; - if (wallElapsed > 0.0) { - const double cap = kWallBleedFraction * wallElapsed; - if (cap < advance) { advance = cap; } - } + const double cap = (wallElapsed > 0.0) + ? (kWallBleedFraction * wallElapsed) + : 0.0; + if (cap < advance) { advance = cap; } step = advance / static_cast(nElems); - /* Unreachable with a finite positive dt — kept because - * downstream monotonicity must not depend on that - * argument holding for every value off the wire. */ + /* Reached whenever the cap is zero, and a backstop + * against a nonsensical dt off the wire: downstream + * requires strictly increasing stamps, so the burst must + * still advance by something. */ if (!(step > 0.0)) { step = dt * kMinBleedFactor; } base = st.lastEmittedEnd + step; } @@ -2847,7 +2937,37 @@ bool FrameDecoder::timestamps(const FrameView& f, uint32_t idx, /* No declared rate: need hrt-derived dt. */ if (!hrtFit_.ready() || f.hrt == 0u) { - return packetBurst(idx, nElems, wallNow, tsOut); + const bool ok = packetBurst(idx, nElems, wallNow, tsOut); + /* Carry the warm-up's state into the hrt branch, or the handover + * from one to the other is a discontinuity in both directions. + * + * The producer-clock reference (lastAccHrt, prevAccCount) matters + * most. Without it the first hrt packet has no previous tick to + * subtract, falls back to kDefaultDt for its inter-element step and + * latches ClockOffset against wallNow - (nElems-1)*kDefaultDt. + * kDefaultDt is only right when the burst happens to run at 1 kHz; + * at 100 samples per 10 ms packet it is ten times too wide and the + * latch lands 89 ms in the past — permanently, since it is below + * ClockOffset's recalibration threshold. Seeding here means the + * first hrt packet measures a real tick delta and latches correctly. + * + * The emitted-timeline reference (lastEmitted*) then only has to + * cover residual disagreement, but it is what keeps the handover + * MONOTONIC: packetBurst ends its burst at wallNow while the hrt + * branch ends at wallNow - (nElems-1)*hrtDt, and without a previous + * end to clamp against the first hrt packet steps the signal + * backwards by up to a whole burst width. */ + if (f.hrt != 0u) { + st.lastAccHrt = f.hrt; + st.lastAccValid = true; + } + st.prevAccCount = nElems; + if (!ok) { return false; } + st.lastEmittedEnd = tsOut[nElems - 1u]; + st.lastEmittedWall = wallNow; + st.lastCounter = f.counter; + st.lastEmittedValid = true; + return true; } const double rate = hrtFit_.ticksPerSecond(); @@ -2946,11 +3066,16 @@ bool FrameDecoder::timestamps(const FrameView& f, uint32_t idx, * at kWallBleedFraction: only that makes the lead bleed off. */ if (st.lastEmittedValid && base <= st.lastEmittedEnd) { const double wallElapsed = wallNow - st.lastEmittedWall; - if (wallElapsed > 0.0) { - const double cap = kWallBleedFraction * wallElapsed / - static_cast(nElems); - if (cap < step) { step = cap; } - } + /* No wall movement, no wall time to spend: see the same cap in the + * declared branch. Zero rather than "skip the cap", so a run of + * same-tick arrivals cannot advance a full hrtDt per sample while + * the wall stands still. */ + const double cap = (wallElapsed > 0.0) + ? (kWallBleedFraction * wallElapsed / + static_cast(nElems)) + : 0.0; + if (cap < step) { step = cap; } + if (!(step > 0.0)) { step = hrtDt * kMinBleedFactor; } base = st.lastEmittedEnd + step; } @@ -3002,7 +3127,7 @@ set(CORE_SOURCES cd Client/udpscope && cmake --build build -j && ./build/udpscope_tests --gtest_filter='FrameDecoder*' ``` -Expected: PASS, 25 `FrameDecoder` tests — 54 across the whole `udpscope_tests` binary. +Expected: PASS, 26 `FrameDecoder` tests — 55 across the whole `udpscope_tests` binary. If `AccumulatedScalarSurvivesBurstyDelivery` fails, do NOT reach for the hrt fit: with a declared `samplingRate` rule 3 never consults it, precisely because the fit is not ready for the first 32 packets and — since `HrtRateFit` regresses `hrt` against ARRIVAL time — is itself corrupted by the very bursts it would be asked to survive. Check instead that `lastEmittedEnd`, `lastCounter`, `prevAccCount` and `lastEmittedValid` are updated on every emitted burst. @@ -3022,7 +3147,11 @@ If `UndeclaredAccumulatedScalarSurvivesAProducerRestart` fails with the spacing If `UndeclaredAccumulatedScalarRecoversFromABackwardWallStep` fails, the hrt branch's `base <= lastEmittedEnd` clamp has gone back to a bare `lastEmittedEnd + hrtDt`, which is one-directional and leaves an NTP correction or a suspend/resume as a permanent lead. Note the test's geometry is deliberate: the step is 0.6 s (it must exceed `ClockOffset::kRecalibThresholdS` or the recalibration that puts `base` behind `lastEmittedEnd` never happens at all), and it lands after the 256-sample rate-fit window is full. `HrtRateFit` regresses `hrt` against ARRIVAL, so it eventually absorbs the step too, at roughly `step / kWindow` per packet — a much slower second correction that would swamp the measurement if the step were placed early or the run continued for hundreds of packets past it. -If `UndeclaredAccumulatedScalarEndsItsBurstOnArrival` fails by exactly `(nElems - 1) * hrtDt`, `ClockOffset::map()` is being latched against raw `wallNow` again, which puts the burst's FIRST element on arrival while the declared branch puts its LAST one there — two accumulated scalars in one scope, one with a declared rate and one without, then sit a whole burst apart on the shared X axis. The test's 10 ms packet of 10 samples is chosen so the derived period equals `kDefaultDt`: the very first hrt-branch packet has no measurable interval and latches the offset using that fallback, and any other period would bake the difference into the offset for the rest of the run. +If `UndeclaredAccumulatedScalarEndsItsBurstOnArrival` fails by exactly `(nElems - 1) * hrtDt`, `ClockOffset::map()` is being latched against raw `wallNow` again, which puts the burst's FIRST element on arrival while the declared branch puts its LAST one there — two accumulated scalars in one scope, one with a declared rate and one without, then sit a whole burst apart on the shared X axis. + +That test alone is NOT sufficient cover for the hrt branch's anchoring, which is why `UndeclaredAccumulatedScalarCrossesTheHrtHandoverCleanly` exists beside it. Its 10 samples per 10 ms packet make the derived period exactly `kDefaultDt`, the one cadence at which the warm-up-to-hrt handover cannot misbehave, so it passes against a decoder that steps backwards by up to a whole burst at that handover. The handover is the sharp edge here: an undeclared-rate signal is served by `packetBurst` until `HrtRateFit` is ready and by the hrt branch afterwards, and the two place a burst differently — `packetBurst` ends it at `wallNow`, the hrt branch at `wallNow - (nElems - 1) * hrtDt`. + +If `UndeclaredAccumulatedScalarCrossesTheHrtHandoverCleanly` fails on the monotonicity assertion, the warm-up branch is returning `packetBurst()`'s result directly without recording state. Two different fields matter and they fix two different symptoms. `lastEmittedEnd`/`lastEmittedWall`/`lastEmittedValid` are what the monotonic clamp needs; without them the first hrt packet skips the clamp entirely and steps back by up to a burst width (-6.5 ms at 10 samples per 2.5 ms, -0.99 s at 1000 samples per 10 ms). `lastAccHrt`/`prevAccCount` are what stops the failure being merely hidden: without a previous tick to subtract, the first hrt packet has no measurable interval, falls back to `kDefaultDt` and latches `ClockOffset` against `wallNow - (nElems - 1) * kDefaultDt`. At 100 samples per 10 ms packet that is ten times too wide and parks the trace 89 ms in the past permanently, since 89 ms is below `ClockOffset::kRecalibThresholdS`. That is why the test asserts the settled burst still ends on arrival and steps at the true sample period, not just that it never goes backwards — the clamp on its own would satisfy monotonicity while leaving the trace displaced. - [ ] **Step 8: Commit**