Commit Graph
386 Commits
Author SHA1 Message Date
Martino FerrariandClaude Sonnet 4.6 d38ab3f9c9 fix: the hole reservation now runs after the accumulator re-plan, so value_acc_ is always sized to channels.size() before reserve_hole() writes into it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-10-08 18:29:57 +02:00
Martino FerrariandClaude Sonnet 4.6 e9d259ac7f fix: a late frame is now dropped before it reaches the timeline, leaving its reserved hole intact
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-10-08 18:26:26 +02:00
Martino FerrariandClaude Sonnet 4.6 6830b97c02 fix: batch_ is now learned in both stream and relative-time modes so hole reservation fires in stream mode.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-10-08 18:22:32 +02:00
Martino Ferrari 5915fc8421 fix: a lost packet reserves the samples it owed instead of vanishing from the timeline 2026-10-08 18:18:12 +02:00
Martino Ferrari 85ba835d61 test: the mock can release a withheld packet past the reorder window 2026-10-08 17:49:11 +02:00
Martino FerrariandClaude Sonnet 4.6 45958a441a docs: late_resequenced counts held-then-delivered packets, not gaps avoided
Fix the false trailing clause in the udps_stats_t::late_resequenced comment,
move udps_held_t from the tunable-limits section to internal-state next to
udps_slot_t, and add braces to three braceless if bodies to match house style.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-10-08 17:44:13 +02:00
Martino Ferrari 6e79a67252 feat: hold a packet that runs ahead of the sequence, and deliver a late one flagged 2026-10-08 17:34:26 +02:00
Martino Ferrari 57e2f6aa8b feat: relative mode keeps the stream's absolute clock for the export 2026-10-08 17:20:47 +02:00
Martino Ferrari d11e7d48c4 feat: relative mode stamps a sample from its position and the declared rate 2026-10-08 17:06:09 +02:00
Martino Ferrari b645859f64 feat: --relative-time selects the counter-derived clock 2026-10-08 17:03:28 +02:00
Martino FerrariandClaude Opus 4.6 2b89f72960 chore: bump deps/plotter to the slope-graded polyline, untrack the sdd report
.superpowers/sdd/ is scratch and gitignored; that one report was tracked
by accident in an earlier phase.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-08 16:56:37 +02:00
Martino FerrariandClaude Opus 4.6 ec8eae0317 docs: todo.md rewritten around what is left
The four original items are done. What replaces them: the invisible
riser, relative timing, named configurations, frame storage for
forensics, opening exported waveforms, the plot context menu, drag
resize, an event cache with replay, and a preferences dialog.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-08 16:56:36 +02:00
Martino FerrariandClaude Opus 4.6 82180f45f7 feat: the mock can drop and invert packets, and carries a noisy channel
--drop-every/--drop-burst withhold datagrams the counter and stream clock
have already advanced over, and --reorder-every inverts a pair on the
wire; both are deterministic for the reason --glitch-at is, since a probe
asserts the spacing of what it finds and a random count is not a figure.
A `noise` signal gives the dense-trace and intensity-floor paths a input
that is not piecewise smooth.

verification.md records the measured readings, including the one that is
a finding rather than a baseline: a burst past kGlitchPackets (256) is
read as a producer clock glitch, each bridge advances the anchor by one
packet span against a timeline 300 ahead, and four consecutive failures
then throw the stream onto the arrival clock.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-08 16:56:36 +02:00
Martino FerrariandClaude Opus 4.6 cd9657663a fix: the math editor's source dropdown no longer hands a GParamSpec to a value handler
notify::selected passes a GParamSpec* ahead of the user data and
GtkSpinButton::value-changed does not, so on_math_param_changed read the
pspec as its MathDialog* and faulted on the first field it touched --
which is to say on opening the math editor and picking a source.

The probe missed it because filling the form sets the dropdown to the
value it already holds, and GObject emits no notify for that: a channel
sourcing channels().front() picks index 0 on a dropdown already at 0.
math_probe now opens on a channel sourcing the *last* acquired signal,
which does move the selection, and reads back the source the handler
wrote into the node.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-08 16:56:25 +02:00
Martino FerrariandClaude Opus 4.6 219ae91fd8 docs: a spec for the square wave's riser being drawn only sometimes
Two complaints, not one: the glitch is a row dropped before any style
knob is consulted, and the thinness is a choice udpscope made and never
revisited -- it asks deps/plotter for a trace thinner than the library's
own default.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-08 16:56:25 +02:00
Martino FerrariandClaude Opus 4.6 723d1db3d2 docs: implementation plan for resequencing and relative timing
Twelve tasks in five phases, so relative timing ships before any of
the client-side reordering work lands.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-08 16:50:50 +02:00
Martino FerrariandClaude Opus 4.6 d5d1295b26 docs: a design for resequencing late packets into a reserved hole, and for relative timing
The two were asked for together and are one design: the packet counter is what
makes a hole addressable, so relative timing is the coordinate system in which
resequencing is arithmetic rather than inference.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-08 16:38:18 +02:00
Martino FerrariandClaude Opus 4.6 aa8ec1794a fix: a zero-width band fires on equality or not at all, and the text now says which
The earlier correction moved the claim rather than fixing it: four places still
said the float-equality test "never fires on a real-valued signal", and a +/-1
square is a real-valued signal -- a standalone check against a band of [1, 1]
fired 19 times, because `inside` is true at +1 and false at -1, so the signal
changes side on every transition.

All four now state the measured behaviour: an exact float-equality test against
the level, which an ordinary signal never satisfies and a signal sitting at the
level satisfies on every transition. Neither answer is a band, which is the
reason the second spin is clamped an increment away rather than the detector
special-cased -- the same conclusion, now resting on a true premise.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-08 11:39:22 +02:00
Martino FerrariandClaude Opus 4.6 c8f10f59dc fix: a condition leg is reported once it is held, so a working leg cannot read nan
A finite trigger_time() only says the edge was accepted; the mode is Capturing
until the post window fills. Every field the report prints is read from
display_snapshot(), whose stamps span x = 0 only once the event is Held, so
reporting on the latch alone raced the capture: leg 2 printed
`measured=nan at0=nan ... mode=1` on one run and the documented
`at0=-0.500907 earlier=-0.498185 mode=2` on the next. The report now waits for
Held under the same poll budget, and says `latched but never held` when it runs
out -- a different defect from `no latch`, so the line names which.

The readings are corrected against three consecutive leg-2 runs and one leg-3
run: `t0` is a rebased stream stamp and varies every run, so it was never an
assertion; `hits=` likewise tracks the arm phase (12 at one arm, 11 at another).
The mode ordinals were wrong -- `mode=2` is `Held`, not `Capturing`. Shapes 2
and 3 are now measured rather than read from the arm code; shape 1 still is not.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-08 11:34:30 +02:00
Martino FerrariandClaude Opus 4.6 90d5d0e2d2 fix: --glitch-at without --glitch-width now does something rather than silently nothing
--glitch-width defaulted to 0.0, so --glitch-at alone produced no glitch and no
warning — the same class of silent no-op that cost a debugging cycle earlier on this
branch.  The default is now 0.002 s (2 ms), matching the standalone probe run.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-08 11:19:27 +02:00
Martino FerrariandClaude Opus 4.6 728e7fbda5 docs: help pages, probe reference and session docs updated for the three new features
- 08-trigger.txt: names all four condition shapes and the buffer search; fixes the first
  paragraph and 'How to reach it' to match what the panel actually contains (fix 3).
- 10-measurements.txt: adds a Statistics bullet — one observation per recompute, what
  resets the history, what does not (fix 3).
- probe-readings.md: re-quotes condition-probe lines with level2= and mode= fields;
  corrects leg-2 window geometry (0.2 s / 10 % pre, not 1.0 s / 50 %); moves the
  wide-window reason to legs 1 and 3; fixes the search-probe scheduling note
  (search_probe calls condition_probe directly; do not also set CONDITION_MS) (fix 6).
- verification.md: corrects shape-2 window geometry to match the code (fix 7).
- invariants-session-and-export.md, README.md: document the measure_stats session key —
  position, type, default false (fix 8).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-08 11:19:19 +02:00
Martino FerrariandClaude Opus 4.6 090b6bb57d fix: a zero-width band fires on equality, not never; dead accessors removed
- The comment in `feed_window()` and three companion sites claimed a zero-width band
  "never fires, because no sample can be on a different side of it than its neighbour".
  That is false: `inside = value >= lo && value <= hi` with `lo == hi == 1.0` is true at
  +1 and false at -1, so a ±1 square fires 19 times in 20 samples.  The correct reason
  the panel refuses one is that it reduces the condition to a float-equality test on a
  real-valued signal, which in practice never fires (fix 4).
- `EventDetector::condition()`, `BufferSearch::channel()` and `BufferSearch::condition_`
  were confirmed unused by grep and deleted (fix 10).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-08 11:19:05 +02:00
Martino FerrariandClaude Opus 4.6 7aa9cf4664 fix: a rebuild, a spurious observation, and a width limit protect the invariants they were breaking
- A buffer rebuild mid-scan (CONFIG or window change) now calls `search.clear()` beside
  `reset_measure_stats()` at both sites; without it the scan kept stale at_/end_ and could
  report Done over fabricated hits (fix 1).
- `search_marks_label` now carries `PANGO_ELLIPSIZE_END` so the side pane's minimum width
  is bounded; the longest text (~58 chars) was driving the pane width (fix 2).
- `update_measures()` gains a third `accumulate` parameter (no default) so every call
  states its intent; the Statistics toggle now passes `false`, stopping the same window
  being counted twice and deflating sd on repeated toggles (fix 5).
- `search_jump()` records whether `publish_pause_view()` returned an empty snapshot and
  `update_search_panel()` appends "(no longer in the buffer)" to the position label when
  it did (fix 9).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-08 11:18:51 +02:00
Martino FerrariandClaude Opus 4.6 806e168ec4 fix: the condition tooltip says what the dropdown changes, not that it has entries
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-08 10:47:38 +02:00
Martino FerrariandClaude Sonnet 4.6 ffc3ec1b4d docs: the three new features are described where each of their readers looks
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-10-08 10:43:39 +02:00
Martino FerrariandClaude Sonnet 4.6 aefac2d796 fix: the condition dropdown's tooltip names all four shapes without listing them
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-10-08 10:43:36 +02:00
Martino FerrariandClaude Opus 4.6 2ed42fe632 fix: every branch of the marks label counts the marks it can see, so the counts read alike
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-08 10:34:34 +02:00
Martino Ferrari 4a58887cb1 fix: the search label no longer claims an off-screen selected hit is in view
place_search_marks() places the selected hit unconditionally to keep it marked when stepping
to it, but the label must distinguish this case from hits that are actually visible on screen,
so it does not lie about what is in view. add sel_off_view to track when the selected hit is
off screen, introduce a shown count that excludes it, and emit a third label case when only
the selected hit is off screen.
2026-10-08 10:25:27 +02:00
Martino Ferrari 5bb4bd207d fix: the marks are read a frame after the jump, so a placed count is read from the window it is on
The plan's Task 8 named the search probe as its test but never extended it, and leaned on the
exit dump instead -- which stops the tick from Live, where no hit is in view, so it reads
placed=0 for a correct implementation and a broken one alike.
2026-10-08 10:18:44 +02:00
Martino Ferrari db81ab2700 feat: a search's hits are marked on every row, and the panel says how many of them it is showing 2026-10-08 10:11:50 +02:00
Martino Ferrari 8ee4effb70 fix: a truncated scan stays truncated, so a hit list with a hole is not reported clean 2026-10-08 10:03:46 +02:00
Martino Ferrari 415fceca87 feat: the buffer can be searched for the trigger's own condition, and the hits stepped through 2026-10-08 09:56:47 +02:00
Martino FerrariandClaude Sonnet 4.6 2cee85c52b fix: a statistics line elides its own text, the toggle handler is guarded, and the doc comment names both labels
- The statistics label in measures_row() now carries PANGO_ELLIPSIZE_END,
  matching the kind label's own treatment, so a long reading cannot widen
  the pane beyond its measured minimum.
- The toggled handler on measures_stats_check now returns early when
  updating_panels is set; apply_layout() sets updating_panels around its
  gtk_check_button_set_active() call and then sets the Reset button's
  sensitivity directly, so the loaded state is fully applied without a
  spurious schedule_layout_save().
- The measures_row() header comment now names both labels that
  update_measures() rewrites and explains the two-line layout.
- gtk_widget_set_sensitive on measures_stats_reset is null-checked in the
  toggled handler, matching every neighbouring handler in the file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-10-08 09:44:04 +02:00
Martino Ferrari 4f496b14b7 feat: the measurements pane can show how each reading varies, and remembers that it was asked to 2026-10-08 09:34:06 +02:00
Martino Ferrari 1a8218887c feat: a measurement accumulates how it varies, so one reading can be told from a trend 2026-10-08 09:22:35 +02:00
Martino FerrariandClaude Sonnet 4.6 d79810c7ba fix: dead probe fields are removed and the channel name is chosen once, so arming cannot disagree with enabling
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-10-07 18:25:33 +02:00
Martino Ferrari 601923f1d1 fix: a window leg's band comment names the sine it is now read against
The leg moved from the square to the sine when the band proved untraversable
by a +/-1 square; the comment beside the band still described the square.
2026-10-07 18:18:33 +02:00
Martino FerrariandClaude Sonnet 4.6 a00663510f fix: a dropout probe outlasts the sine's own crossings, so only a real silence latches it
- Leg 1: widen to 1.0 s / 50% pre so the square's opening edge is in the snapshot
- Leg 2: use sine (not square) for the window channel, edge = falling so the band exit
  is deterministic, and read earlier from vals[end-2] rather than half the snapshot
- Leg 3: raise timeout to 0.3 s (> half-period), accept either crossing direction,
  widen to 1.0 s / 50% pre to reach the crossing that sits 0.3 s before x=0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-10-07 18:13:26 +02:00
Martino FerrariandClaude Opus 4.6 a26770ec32 feat: a level band and a signal that stops can be the trigger's event too
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-07 18:01:07 +02:00
Martino FerrariandClaude Sonnet 4.6 0876ecfe22 fix: condition-probe assertion uses snapshot width, not mock-clock phase
The old `phase=` field compared `trigger_time()` against the mock's absolute
`glitch_at + glitch_width` target.  The app's stream epoch is the first packet
it received, not the mock's t = 0; the two clocks differ by however many
packets the CONNECT handshake cost, making the comparison meaningless.

Replace it with a snapshot-relative measurement: scan backwards from x = 0 in
the held event snapshot for the most recent upward crossing of `condition.level`
(the same `prev < level && val >= level` test the detector uses) and negate the
crossing's stamp to get the measured pulse width.  A `measured` near 0.002 s is
the glitch caught exactly; near 0.200 s means an ordinary edge was latched.

Remove the now-dead `glitch_at`, `glitch_width`, `square_period` fields from
`ConditionProbe`.  No other files changed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-10-07 17:48:25 +02:00
Martino Ferrari d147678b1c feat: a pulse whose width is wrong can be the trigger's event, so a glitch can be caught 2026-10-07 17:32:11 +02:00
Martino FerrariandClaude Sonnet 4.6 b3fd9ae00f refactor: what counts as an event moves into its own evaluator, so a search can ask the same question the trigger asks
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-10-07 17:15:37 +02:00
Martino Ferrari 178cf54ef6 feat: the mock can inject a narrow pulse and stop a signal, so a width and a dropout condition have something to find 2026-10-07 16:59:29 +02:00
Martino FerrariandClaude Opus 4.6 9aa1d80158 docs: an implementation plan for the three new scope features
Nine tasks with literal code: the mock's injected faults, the shared
EventDetector the trigger and the search both run, the four condition
shapes and their panel rows, the Welford statistics and what resets
them, the chunked buffer search, its marks, and the documentation.

Every probe's expected reading is written as a shape with the measured
figure left to be recorded from a run, never assumed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-07 12:27:30 +02:00
Martino FerrariandClaude Opus 4.6 a5ca841603 docs: the spec names the formatter the code actually has
format_measure_value() does not exist; the shared SI-prefix formatter is
format_measure(double, const std::string&) at src/ui.cpp:1004.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-07 11:21:36 +02:00
Martino FerrariandClaude Opus 4.6 19aad492eb docs: the trigger is not persisted, so the condition is not either
The spec described the new condition fields as appended session keys. LayoutState
carries no trigger field at all -- the trigger is built from --holdoff and the
panel's defaults on every launch -- so there was nothing to append to. Persisting it
is a separate feature nobody asked for, and a session that silently restored an armed
pulse-width condition would surprise rather than help. The statistics toggle stays as
the one session key this design adds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-07 11:11:30 +02:00
Martino FerrariandClaude Opus 4.6 6770c6a648 docs: a zero-width trigger band is refused, not degraded to an edge
The spec claimed a window condition with equal levels degrades to an edge trigger on
that level. It does not: the band test is a change of side, so equal levels make the
signal "inside" only on exact float equality and the condition silently never fires.
Special-casing the detector into an edge trigger would conflate two state machines
that answer different questions -- the divergence the shared evaluator exists to
prevent. The panel refuses the degenerate band instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-07 11:09:52 +02:00
Martino FerrariandClaude Opus 4.6 98be03e00d docs: three scope features are specified around one shared event condition
Qualified trigger conditions (pulse width with glitch as a comparison, window,
dropout), per-measurement statistics with a reset, and a search over the retained
ring. The first and third are the same question asked of live and of recorded data,
so they share one allocation-free EventDetector extracted out of TriggerEngine --
and the probe that proves the two paths agree on t0 is part of the scope.

Reference waveforms, PNG export, FFT and a byte-bounded event history are out, each
for a stated reason.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-07 11:07:00 +02:00
Martino FerrariandClaude Opus 4.6 f179fbbb44 fix: a clock glitch is bridged and a channel is known by name, so a capture keeps its t0
Two defects behind one report ("a trigger during a capture overrides the first,
and the hold-off does not apply"); the second is also what made a spike in one
channel look like the acquisition resetting.

The trigger engine keyed its invalidation on the trigger channel's *index*, which
every rebuild of the channel list renumbers -- enabling a signal that sorts before
it is enough. Any rebuild therefore abandoned an open capture and set no hold-off,
so the next edge inside the post window became t0. It keys on the channel's name
now, which changes only when the source genuinely does.

A packet whose stamps jump more than 256 packet spans ahead is now rejected as a
fault and *bridged* at the declared cadence, instead of poisoning the monotonic
mark (which pinned every later packet flat at the glitched value) and condemning
the next good packet. Only four consecutive failures abandon the declared time
signal, and that fallback no longer wipes the rings: the arrival clock is anchored
to continue from the monotonic mark, so the history is still on one timeline --
the wipe is what made one glitching packet look like the stream restarting from 0.
The declared unit is resolved before those checks, since they weigh the stamps
against a cadence in seconds.

Guards: UDPSCOPE_PROBE_TRIGRENUM_MS prints t0_held=1 through the real widgets,
and the mock's --time-glitch-at/--time-glitch-by injects the clock fault.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-07 10:29:05 +02:00
Martino FerrariandClaude Opus 4.6 4003084648 feat: a signal's chip is clickable, so a trace's colour is the user's to choose
Clicking the coloured chip in the signal list opens a GtkColorDialog and the
colour it returns overrides that signal's palette entry. Held by name, like
every other identity here, and persisted as a session `[[color]]` table.

RowSet::signal_color() becomes the one resolver: install() builds the trace
through it and all three chip sites read it, because a chip exists to say which
trace a row is about and an override is exactly the state in which two routes to
a colour would disagree. A grey chip names no trace and so is not clickable, and
the way back is `Default colour` in the signal's menu -- a setting with no undo
is a trap.

The gesture sits on the existing label rather than in a GtkButton: padding on
this row is what made the signal list twice as tall once. It claims its sequence
so a click on the chip cannot become the start of a row drag.

UDPSCOPE_PROBE_COLOR_MS reads the result out of plot::Plot::trace() and out of
the chip's own markup, with an override in no palette slot, so neither leg can
pass by asking the resolver what it already said.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
0.2.1
2026-10-07 08:22:32 +02:00