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>
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>
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>
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>
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>
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>
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>
`math_dialog_sync_form()` wrote the label and then moved a node whose source
names an unacquired channel -- a mutation that changes what
`math_dialog_problem()` reports. With the sync re-entry now guarded, the nested
call that used to repair the label is suppressed, so the line kept describing a
source that had just been fixed and left `Apply` insensitive until the next
mutation. The reconcile moves above the write and the dropdown reuses its list.
A review of the whole node editor. The hit test measured the distance to the
24 curve samples rather than to the segments between them, so a wire spanning
several rows was unpickable between two samples -- the probe now places its
boxes 280 units apart and prints the nearest sample's own 8.4 against a 3.0
tolerance, which the point test would have missed. A refused connection said
nothing: the message is now carried in `MathDialog::refusal` and shown once by
the single writer of the label, and a self-wire is refused through the real
drag-end path instead of only through `math_connect()`.
Two defects were found only by the new assertions. `math_dialog_sync()`
re-entered itself -- `gtk_drop_down_set_selected()` emits `notify::selected`
for a programmatic set too, and the handler answers by syncing again, which
consumed the refusal before it could be drawn -- so the body is now guarded
one level deep. And a deleted node left `selected_wire` naming an index that
afterwards belongs to a different wire, so `Delete` cleared an input on the
wrong node.
A node is placed in the first free grid cell rather than at the end of the
list's own cell, so a node added after a delete no longer lands on top of a
survivor; the label ellipsizes with a tooltip rather than wrapping, because a
wrapped line makes the dialog's height depend on the text in it.
Each invariant records what went wrong rather than only the rule: a port hit
that lost its node, a hit test at the identity view that asserts nothing, a
`Delete` that must not fall through to the selected node, and a column pitch
too narrow for a wire's own tread. The warning baseline drops to four.
The file was 1967 lines and is auto-loaded into every context, so every
session paid for the whole of it to reach one invariant. It now carries the
project's goal, the three facts that are always relevant, and a table of
contents; the body is fourteen files under docs/claude/ reached by link
rather than by import, so nothing is loaded until it is asked for.
The content is moved verbatim -- the only edits are each file's own header
and the cross-references that became cross-file ("see the invariant below"
is now a link to the file that owns it).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Nine tasks, each ending in a commit: the silent disarm in the port hit test,
the view transform, the drag-based wiring, wire deletion, the topological
re-sort that makes a backwards wire legal, the live validation line, the op
picker, tidy, and the docs with the full verification run.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The reported "cannot add edges" has a specific cause: the output port's hit
radius reaches past the box it belongs to, and the press handler re-derives the
owning node with a box test, so clicking the visible dot's outer half sets
pending_out to -1 and silently disarms. Edge deletion has no code path at all.
The design replaces the two-click protocol with a drag gesture, makes the list
order the editor's own invariant via a topological re-sort rather than a rule the
user must obey, adds a view transform with every hit test routed through its
inverse, and trades the modal alert on Apply for a live validation line. It also
records that the Cancel button still calls gtk_window_destroy -- the dangling
dialog pointer CLAUDE.md documents as fixed was only fixed on the Apply path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two code-review findings:
1. Add a guard in cmake/install_desktop_entry.cmake that fails visibly when both
XDG_DATA_HOME and HOME are unset or empty, preventing silent installation to
the filesystem root in minimal CI sandboxes.
2. Replace the plan's tail-based verification with grep, because cmake's
FATAL_ERROR message and ninja's "build stopped" lines push the actual
rejection message out of the last four lines. The grep command reliably finds
the expected "desktop-file-validate rejected" string.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Five tasks, each ending in a commit: the two SVGs with build-time xmllint
validation, the window icon plus the registration move and the app icons:
assertion, the AdwAboutDialog migration, the opt-in desktop-entry target, and
the documentation with the full regression run.
Written assertion-first, since the project has no unit suite: each task's first
step adds the probe or dump line and watches it read MISSING. Both negative
tests are real -- one breaks an SVG comment and requires the build to fail, the
other breaks Type= and requires the target to FATAL_ERROR.
The spec asserted that a paintable's intrinsic size reads 0x0 for an SVG
librsvg refuses. It does not: gtk_icon_theme_lookup_icon() never returns NULL
and always reports the requested size, and a valid icon, a malformed one and an
absent name all snapshot to the same texture node. GMarkup accepts the double
hyphen, and gdk-pixbuf goes through glycin here and refuses our own icons.
So the malformed file is caught by preprocess="xml-stripblanks", which runs
xmllint during glib-compile-resources and fails the build with file, line and
column. The runtime line keeps its format but asserts only what it can: the
name resolves, the resource is present, and the root element declares the size.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Records the decisions taken before design — the application ID stays
org.udpscope.app, the icon is the instrument face, and the desktop entry is an
opt-in CMake target rather than an install() rule — along with the HIG
constraints the artwork is built to and the two silent failures the new
verification line has to tell apart.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Eleven tasks over the approved spec: build info and About, the plotter's
ctrl/alt axis-restricted zoom, 14 help pages, the accelerator table and
overview window, the app key controller, and the two-tier status bar.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Four features the user asked for together: an About dialog (version, build
timestamp, settings path), a shortcut overview covering the keyboard and the
mouse, a fourteen-page help window, and a status bar split into a short glance
line plus a details popover so the plot's own row stops being crowded.
Records one deviation from the approved design: keyboard zoom-x/zoom-y use
ctrl/alt rather than shift, because `+` *is* shift+`=` on the common layouts and
a bare zoom-in therefore already arrives with Shift set.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Seven tasks, each ending in a probe assertion rather than a unit test -- this
project's regression net is the headless probes, not a suite. The spec's
multicast round trip is split across three tasks so nothing in the plan needs a
group this machine can join.
Launched with no --server, udpscope silently connects to 127.0.0.1:8000 and
offers no way to pick a source or see which ones it already knows. The design
is a pre-flight dialog: it runs before ScopeModel exists, so a cancel costs no
buffer and no socket and the existing pipeline is untouched.
The list is built from the stored session files, filtered to those naming at
least one signal -- a development machine accumulates one file per probe run on
a throwaway port, 140 of them here -- and the session format gains multicast and
interface keys so a row can reconnect exactly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>