Round 4 of Task 4 review. Four defects in FrameDecoder's rule 3:
- The undeclared-rate (hrt) path positioned each burst at an ABSOLUTE
hrt/ticksPerSecond(). hrt counts from the producer's boot, so it is ~1e11
ticks by the time a scope attaches, and the rate is refitted every packet
with a few parts in 1e4 of wobble. The product is tens of milliseconds of
jitter in BOTH directions -- not merely imprecise, non-monotonic. Integrate
short tick deltas into accProdSec instead and let ClockOffset latch the
epoch that leaves behind.
- The lead bleed used a fixed 0.9 factor, which converges only while the
declared rate is within ~10%. Squeeze proportionally to the excess instead
(floored at kMinBleedFactor), settling it in a single burst.
- A single-sample flush fell through to the plain-scalar rule, dating it from
arrival and leaving lastCounter stale so the next real burst reinstated a
hole that never existed. Accumulate mode flushes on a timer, so a short
cycle legitimately yields one sample; keep it on the chain.
- kMaxCounterGap was inert: an absurd gap yields an absurd prediction that the
arrival backstop already rejects, and no input can distinguish the two
rules. Removed rather than left implying a behaviour it did not have.
FrameDecoder.h now states the deliberate divergence from StreamHub -- which
converts hrt with the LOCAL MARTe timer frequency, valid only because it runs
on the producer's host -- and why a remote scope's drift is irreducible.
Three new tests, each sabotage-proven non-vacuous: producer restart, short
flushes staying on the chain, and a 20000-packet undeclared run after a
day of producer uptime that asserts SPACING as well as ordering (the
monotonic guard alone restores order while leaving positions wrong).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Round 3 of the Task 4 review. Three defects, all in FrameDecoder rule 3.
The resync backstop was one-directional. `predicted` is never below
lastEmittedEnd + dt, so rejecting a correction that would step backwards
meant only a LAGGING chain could ever be pulled back; a chain running fast
drifted ahead without bound. Two hosts' crystals differ by tens of ppm, so a
declared SamplingRate is always slightly wrong in one direction or the other
and this is certain on a long session. A leading timeline cannot be corrected
in one burst without going backwards -- lastEmittedEnd is by definition past
arrival -- so the excess is bled off by drawing each burst 10 % narrower until
the timeline is back inside the threshold.
A repeated packet counter was treated as a normal packet. The C client
de-duplicates fragments only, so an unfragmented update reaching a host that
joined the group on two interfaces was emitted twice, doubling the values and
advancing the timeline by a burst that never existed.
The samplingRate == 0 path differenced two HrtRateFit::toSeconds() results.
toSeconds() divides an absolute tick count -- ~1e11 on a producer that has
been up a day -- by a rate refitted on every packet, so its few-parts-in-1e4
wobble arrives multiplied by the whole elapsed epoch: tens of milliseconds of
jitter on a value whose consecutive difference is a few milliseconds. Raw
ticks are differenced instead, anchored on the first usable packet so the
wobble applies only to the interval since attach.
The existing hrt-gap test could not have caught the last one: its 10 ms
producer period made the expected answer exactly kDefaultDt, so a decoder
that derived nothing passed. It now uses 25 ms.
Four tests added, all sabotage-proven. The plan is updated to match.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Review found the decoder was estimating something the wire states exactly.
FrameView::counter increments once per update, so a gap of g means g-1 lost
datagrams; reinstating their duration restores the hole precisely, with no
threshold and no dependence on arrival time. The arrival-anchor comparison
survives only as a backstop for what the counter cannot express — a producer
restart, a counter stuck at zero, a wrong declared rate — and can no longer
step a signal's timestamps backwards, which the ring and trigger forbid.
Also from review: guard the time-signal lookup against a frame carrying more
signals than the installed table, and give FrameBuilder a counter parameter.
Leaving it at zero had hidden the counter rules from every test, and made the
hrt-gap test vacuous — under uniform arrivals the hrt path and packetBurst
agree by construction, so it could not tell which branch answered. Its
arrivals now carry zero-mean jitter.
Each new assertion was proven non-vacuous by sabotage: dropping the gap term,
the backward guard, or the hrt branch fails exactly its own test.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Forward-chaining each accumulated burst onto the previous one suppresses
arrival jitter, but an unchecked chain never recovers: one lost datagram, or a
declared sampling rate that differs from the producer's real one, dates every
later sample early for the rest of the run. The chain is now a prediction,
compared each packet against the arrival anchor and abandoned beyond
kBurstResyncThresholdS, which bounds the error instead of accumulating it.
Plan amended so the hrt-fit fallback (unusable here: the fit needs 32 packets
and is itself corrupted by bursty arrivals) cannot come back.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The jitter test fed a receive timestamp that went backwards (1001.02 then
1000.97), putting the second reading 1.03 s from the prediction — twice the
threshold, so not jitter under any reading. That is a digit slip for 1001.97.
It had been worked around by making the threshold one-sided, which passes the
test but never fires when the producer's clock steps forward: the prediction
stays ahead of the wall clock, the error stays negative, and the trace sits in
the future for the rest of the run. Restored std::fabs, corrected the test data,
and added the forward-jump case that the one-sided version silently failed.
Plan amended so the bad data does not come back.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The ramp data in EmitsPointsInTimeOrder never produced a bucket whose maximum
preceded its minimum, so an implementation ordering the emitted pair by value
instead of by time would have passed. Replaced with an explicit two-bucket case
whose second bucket reverses the order.
file(COPY) is a hard configure error on a missing source, so a checkout without
the sibling StreamHub resources failed to configure despite the ASCII-icon
fallback the block above had just selected.
Plan amended to match on both points.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Each of these would have surfaced as a compile/link failure or a reviewer
rejection mid-execution, when the implementer holding the task has no view of
the neighbouring task that contradicts it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Eighteen TDD tasks covering the build scaffold, pane tree, time base, frame
decoding, trigger FSM, threading, UI, persistence and export, so the scope can
be built task-by-task with a reviewable deliverable at each step.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Plan derived from the approved 2026-07-01 design spec; covers scenario
kind unification (chain/direct/recorder/debug/tcplogger), instrument-first
double-run coverage, and DebugService/TCPLogger E2E via debugclient.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>