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>
6.6 KiB
6.6 KiB
Invariants: the time axis and the window control
What may change the time scale (one function, one caller), how the x labels pick their form, and what a fit restores.
Part of CLAUDE.md. These are rules that cost a defect to learn; read the ones touching the area you are editing before you edit it.
- The window control's label is width-locked and ellipsized (
kWindowLabelChars), for the same reason as the status label: it sits in the header bar above the plot, so a label that grew from1 sto123.456 mswould widen the bar and relayout everything under it on a mere change of window. That is also why the window is typed into a popover rather than shown in the bar. - No plot interaction changes the time scale; only the window control does. The time scale
is more than the axis:
RowSet::window_s_also feedsScopeModel::set_event_span()(the trigger's pre-trigger share is a share of this window),ScopeModel::set_target_span()(which may re-plan the partition, move the decimation and drop the history), the window control's readout and the session file. A zoom or a pan does not touch it by construction — the controller writes the member axis andclamp_to_data()caps the span atwindow_s_, so nothing has to be written to keep it that way. Neither do the two fits: a fit is the undo of a zoom, soRowSet::fit_x()restores the window that is already set and readswindow_s_without ever writing it. Both fits used to frame the data instead and movewindow_s_alone, which was wrong twice over — a gesture-triggered re-plan of the rings, and an event span left on the old window until the next decade step or unit change applied the fitted value for real (one action, two changes of time scale). apply_time_window()is the only place the time scale changes, and the window control is its only caller. It clamps towindow_limit()and movesset_target_span(),set_event_span(),RowSet::set_time_window(), the control and the session save together — the partition before the axis, so the axis is framed over the rings that will exist. A fit must not come here for two reasons: it is not entitled to the scale at all, andset_time_window()unpins and re-arms the live follow, which would throw a held event or a paused view onto a live edge it has nothing bound to. Paths that change the span for their own reasons (an event pin) callsync_window_display()alone, which only restates the control and never moves the plot — callingapply_time_window()there would clamp a legitimately short capture up to the requested window.- The x labels go relative on the tick step against the label resolution at the axis'
magnitude, never on the span.
RowSet::update_time_labels()'s question is only ever "can this step be written at this magnitude":TickFormat::Timecarries three significant figures below a minute, whole seconds throughm:ss/h:mm:ssup to a day, and three figures of days above — so the resolution is a step function of the magnitude, whichtime_label_resolution()states and nothing else may re-derive. The old test wasspan < 1.0and it was wrong in both directions: a 2 s window at twenty minutes of uptime reads20:00on every tick and stayed absolute, and a triggered sub-second window — whose axis is already[-25ms, +25ms]about the edge — went relative and so re-anchored its ticks away from the trigger, with at0caption that had nothing to explain. Three details hold it up. The step is read back from the majors the last layout produced, which is not a feedback loop (a tick's value depends on the range and the axis' pixel width, never on the label form) but is absent before the first layout — and with no ticks the form must be left alone, because a step guessed from the span would be latched by the hysteresis below. The hysteresis is a 1.05 band on leaving the relative form. And entering it needs a ULP of slack (1 - 1e-9): a step differenced from two tick values and a resolution frompow(10, n)differ in their last bits when they are the same number, andstep == resis a readable axis (1.73s,1.74s,1.75s— one in the last digit is exactly enough), so the bare comparison picked a different form from one frame to the next. The guard is[timelabels-probe]'sdistinct=1, which asserts what the form is for rather than which form a given window gets. - The
t0 = …caption is chrome, never an annotation. It isPlot::set_x_note()(added todeps/plotterfor this), drawn byRenderer::draw_x_note()on the legend's owndraw_readout_panel()backplate in the bottom-left corner of the axes, in pixels. It was anAnnotationfirst, and an annotation's anchor is a data coordinate: the caption therefore crawled across the plot under a pan and drifted under a zoom, which is what "[t0=...s] move slightly around when user zoom/pan" was. Three things follow. The text is the host's, because the caption has to name the anchor precisely enough that label + caption is the exact absolute time andformat_time()would round it to three figures ("t0 = %.6g s"); only the bottom row of the figure carries one, every other row being set to""; and the renderer measures it rather thanPlot::layout()— it reserves no space and nothing can be picked on it, so there is no rect to read back, and a caption explaining the x labels must not be able to move the layout those labels were measured into. The guards are the library'sthe_x_note_is_chrome_that_a_pan_or_a_zoom_does_not_move(the footprint is the pixels a frame with the caption differs from one without, and a pan plus a 64× zoom must give the same box to the pixel) and[timelabels-probe]'snote=, which readsPlot::x_note()back. fit_x()restores the whole of what the view may show, which is not alwayswindow_s_. Live, that is the newestwindow_s_at the live edge (set_time_window(window_s_), the same path a resume takes). Pinned — a held event, a filling capture or a paused view — it is the pin's full range, which is the snapshot's own stamps and may be legitimately narrower than the requested window. It must write the per-row member axes as well as the link group:hold_pinned_window()prefers a member that differs frompinned_stated_, which is how it tells a real gesture from its own echo, so writing the group alone is reverted to the zoomed range on the next frame.fit_all()is the y fit plus this, shared rather than duplicated so the two cannot drift apart.