Commit Graph
8 Commits
Author SHA1 Message Date
Martino Ferrari 22e7a8efcc measure: the split's own means are back as above/below/between
`top`, `bottom` and `amplitude` became the *flat levels* in e83ad86 -- the level the
signal dwells at, read from the density of the window's buckets -- and that change kept
the split's own two means as its internal fallback for a window the pyramid cannot
summarize. Those means are now three types of their own, so the two different questions
about one split can be asked side by side: `above %` and `below %` are the plain mean of
the samples on one side of a share -- the reading the old `top`/`bottom` gave -- and
`between % %` is the mean of the band between two shares, which no single split can
answer, because a band's two edges are two thresholds.

Nothing about the arithmetic is new, and nothing is a new pass. `Above`/`Below` are the
`sum[]`/`count[]` split `measure_accumulate()` already folds -- the very figures
`Top`/`Bottom` fall back to -- and `Between` is the **difference of two levels' upper
sides**, both of which come out of one pass two over one window. It rides the bucket
fast path unchanged: a bucket wholly inside the band is strictly above the lower level
and strictly below the upper one, so both accumulators fold it whole, and only the
buckets that straddle either edge are opened. `measure_level_pcts()` is the one place the
shares are read, and it clamps both and puts `between`'s pair **in order**, so "between
80 % and 20 %" is the band between 20 % and 80 % -- normalized rather than refused,
because the shares name two edges rather than a direction. Nothing to measure reads `--`,
as for every other type: a share at 100 % has no sample above it, and a band can hold
none at all.

Measured, `[bandavg-probe]`, fast mock at a 2 s window (`--rate 1300000 --batch 1000
--freq 1`):

  sine   `above 50` 0.636620 (2/pi), `below 50` -0.636620, `between 50 75` 0.255873
  square `above 50` 0.986496 (30 harmonics) / 1.000000 (--square-harmonics 0),
         `between 50 75` 0.302744 / --
  ramp   `between 50 75` 0.250000 and `above 50` 0.500000, `above 100` --

The first three are the figures the old `top`/`bottom` documented; the ramp's are what a
uniform density predicts (a band's mean is its own centre); the ideal square's empty band
is the nothing-to-measure rule. The pane offers the types and edits their percentages
through the dialog's second share -- which only `between` shows -- and a measurement's
pair survives add/edit/delete and the session file (`threshold2`), where a file written
before it existed loads at the default.

No raw pass: with the measurements probe adding one of every type at a 2 s window, `raw`
went from 113966 of 240719031 covered (0.0%) to 134978 of 255372033 (0.1%), 0.8 -> 1.1
ns a covered sample.

Docs: the band-average invariant (and the two assertions the flat-level change moved),
the probe readings for the new probe, the verification recipe, the manual's measurements
page and the README.
2026-10-15 07:56:37 +02:00
Martino Ferrari e83ad86b8f measure: top/bottom/amplitude read the flat levels, not the extremes
A bench scope separates Vtop/Vbase from Vmax/Vmin because a band-limited
edge overshoots and rings: the mock's `square` at 30 harmonics reaches
±1.179 where its plateau is ±1. On the rippled square the old readings were
a property of the edges rather than of the waveform -- `top` 0.986496,
`amplitude` 1.179350 -- and `amplitude` reported the overshoot.

`top`, `bottom` and `amplitude` are now the flat levels and their delta.
A flat level is a density question -- the plateau holds most of the
samples and the ringing a few percent -- so `measure_scan_buckets()` folds
a 64-bin histogram of the whole buckets' midpoints while it has the list in
hand (one observation per bucket, each bin carrying its buckets' exact sum
and finite count), and `measure_flat_levels()` takes, either side of the
split, the densest bin holding at least half the densest one's buckets --
the outermost of those, which is what makes a waveform with no plateau read
its peak -- and the exact mean of the samples of the buckets within 5 % of
the window's peak-to-peak of it. 5 % is deliberately under the ~9 %-of-swing
Gibbs overshoot. `amplitude` is `top - bottom` (a scope's Vamplitude), not
the half-swing it was.

Measured, fast mock, 2 s window, harmonics 0 -> 30: top 1.000000 ->
0.999983, bottom -1.000000 -> -0.999983, amplitude 2.000000 -> 1.999966,
while `peak to peak` keeps the extremes (2.000000 -> 2.358701). On the 1 Hz
sine top is 0.957988 (it was 0.636620, the half-wave mean; a sine has no
plateau, so the densest band is at its peak) with sd 0.0014 over a scrolling
window, and `peak to peak` is still exactly 2.

No raw pass and no new fetch: the histogram is the same fetched bucket list
read a second time, gated on a pane that holds one of the three types.
`raw` is unchanged (0.1% clean, 2.1% with the --noise 0.1 interferer,
against 0.1%/2.1% before) for 0.44 -> 0.50 ms and 0.52 -> 0.65 ms a pass.
A window the pyramid cannot summarize -- an hf-filtered walk, or one shorter
than a bucket -- keeps the old readings: the mean of each side of the split,
and the window's peak to peak for amplitude.

Docs: the flat-level invariant (and the two 2/pi assertions it replaces),
the probe readings, the manual's measurements page and the README.
2026-10-15 01:19:33 +02:00
Martino Ferrari 41cd8e5f38 help: recommend the system-wide install and record the location trap
A Mallard set under $XDG_DATA_HOME/help is one Yelp cannot draw the figures
of: its sandbox exposes the system help directories but not the user's data
directory, and each figure is a separate file the page references. The pages
open without their images, and through the app's help: URI the viewer spins
for ever because the failed image fetch means the page's load event never
fires -- which reads as a navigation failure, not a missing resource.

The alert and the manual both recommended install-help-user as the no-root
route, so they led users into exactly that case. Name the system-wide install
first and state the user-local caveat where the advice is given, and record
the finding in the GTK traps invariant so the next figure that will not draw
is not chased as a markup or a file problem.
2026-10-15 00:17:38 +02:00
Martino Ferrari 6138e6bdce fix: the figures carried a chunk WebKit refuses, which is why a page never loaded
The user's report was precise and I misread it twice: opening trigger.page as a *file* rendered the
page with no image, while opening it through the app's `help:` URI spun forever. Both are one cause.
The three figures carried a `bKGD` chunk -- and the PNG specification does not permit `bKGD` on an
image with an alpha channel, which all three are (colour type 6). ImageMagick both writes and reads
it, so my "decodes fully" check proved nothing: I tested with a decoder more permissive than the one
that has to load the file. WebKit rejects it, so the image never appears; over `help:` the fetch
never completes either, so the page's load event never fires and Yelp's spinner never stops.

It came from `import`, which records the root window's background. Every figure now carries only the
chunks a decoder is guaranteed to accept (IHDR/IDAT/IEND), and `tools/make-help-shots.sh` normalises
its output at the end so a re-shoot cannot reintroduce it -- with the reason written where the next
reader will be, since "ImageMagick reads it" is exactly the trap.

`tools/check_help.sh` gains the rule, because the four yelp-check subcommands and the structural
rules are all blind to a figure no decoder will accept: a `bKGD` on colour type 4 or 6 is a finding,
checked directly rather than through a permissive tool. It is a whitelist because a decoder is one.

Verified: the rule fires on the committed figures (colour type 6, bKGD present) and passes on the
fixed ones; the figures still decode and are still 1280x800 / 938x420; check_help.sh is green on the
source and installed sets; make -C build test-help passes.
2026-10-15 00:01:52 +02:00
Martino Ferrari fe499bfe41 help: the search block's (i) on its header, a 12 px mark, and a real search page 2026-10-14 23:29:00 +02:00
Martino Ferrari 395c9caec3 help: send readers to the install targets, in the alert and the manual
The failure alert told people to run `cmake --install build --prefix ~/.local`;
that still works, but `cmake --build build --target install-help-user` is the
form that needs no prefix guessing and prints the path it wrote, so the alert
names it and stays its former length. The manual gains a short "Installing
this manual" section on the overview page naming both targets -- the no-root
user one and the prefix-driven one for a system-wide or packaged install,
which the caller wraps in sudo -- and README and the GTK invariants doc follow
the alert's new command.
2026-10-14 22:30:07 +02:00
Martino Ferrari e140642ed0 fix: the manual's figures carry a description, and the check that would have caught them
Three pages opened in nothing in Yelp -- overview (twice), math, trigger -- while every check
passed. They are exactly the pages whose <figure> had a sentence for a <title> and no <desc>:
a Mallard <figure> is title?, desc?, media, and the page is refused without the description. The
idiomatic shape, as the shipped sets on this machine write it, moves the sentence into <desc> and
leaves the <title> a name. Note trigger-capture.png is referenced from both overview and trigger,
which is why those two pages are the ones that were noticed.

The gap was in the check, not in its reach: tools/check_help.sh ran links/media/orphans/ids and an
HTML render, and all four are silent about a page that is well linked, well parsed and
unrenderable. `yelp-check validate` is the one that would have caught it, and it cannot run here:
it generates the RelaxNG schema with xsltproc, but the schema includes the base grammar over the
network and this machine's libxml cannot fetch. So the structural rules this project has actually
needed are checked with xmllint's XPath, offline, each written after a page needed it.

One trap, paid for while writing it: a Mallard page declares
xmlns="http://projectmallard.org/1.0/", XPath 1.0 has no default namespace, so `//figure` matches
nothing and every rule passes -- the first version of this check reported "ok" for the very defect
it was written for. Every expression uses local-name() for that reason, and the script says so
where the next reader will be.

Verified: on the committed pages the rules exit 1 and name the three figures, which is the
failing-before case kept as the regression test; on the fixed pages all five rules pass and
yelp-build renders all 15; make -C build test-help goes from red to green.
2026-10-14 17:15:31 +02:00
Martino Ferrari c1460a6977 feat: the manual is a Mallard set opened in Yelp, not a GResource in the binary
The help was 14 Pango-markup `.txt` bodies in a second GResource, drawn into a paged `GtkWindow`
built in `help_window.cpp`. That made F1 a viewer of ours with no search index, no cross-references
and no translation, and it kept the manual in the *binary* when every other desktop application's is
a document on disk. It is now a real Mallard set:

  help/C/org.udpscope.app/  index.page + 14 topic pages + figures/*.png

and `show_help()` hands `help:org.udpscope.app` to whatever the desktop registered for the `help:`
scheme -- Yelp on GNOME -- through a `GtkUriLauncher`, reporting a launch failure with a
`GtkAlertDialog` rather than a click that silently does nothing. The paged window's builder, the
page table and `help_self_check()`'s markup validation are gone with the resource.

The document identifier is the desktop file's own basename (its desktop-file id), derived in
CMakeLists from `data/org.udpscope.app.desktop.in`, so the same string is the `.desktop` name, the
GApplication id and the install directory at once. The set is installed by a new `install()` rule to
`<datadir>/help/C/org.udpscope.app/` -- where the freedesktop Help System Specification's `help:`
URI resolves `help:<id>` -- so the manual is read from a datadir by another process even though the
binary has no install step. `UDPSCOPE_HELP_NAME` and `UDPSCOPE_HELP_DIR` are compiled in from that
one place.

The 14 pages were migrated fact for fact, but restructured the Mallard way: a `<desc>` per page
(what a search result shows), `<steps>` for procedures, `<terms>` for option lists,
`<gui>`/`<key>`/`<keyseq>` for interface words and keys, `<note>` for the asides, and a topic graph
via `<link type="guide" xref="index#main"/>`. The figures are annotated screenshots produced by
`tools/make-help-shots.sh`, which drives the app headlessly against `tools/mock_udpstreamer.py` and
draws the boxes with ImageMagick; the side panel's box is measured from the app's own `side=NNNx`
dump reading, the header and status bands come from a documented table, and a crop whose standard
deviation is 0 fails the script.

`cmake --build build --target test-help` checks the set the way `test-ui` checks the `.ui` files:
`yelp-check`'s offline subcommands (links, media, orphans, ids) and a `yelp-build html` render.
`yelp-check validate` is deliberately absent -- it fetches the Mallard RelaxNG schema over the
network and fails offline for that reason, not for a fault in the pages.

The `[help]` probe reading changes with the resource it was written for. `parsed == pages` over
in-binary markup cannot exist now, so `help_self_check()` scans `UDPSCOPE_HELP_DIR` and the line
reads `topics=14 index=1 figures=3`; `topics=0 index=0` is the manual missing from the tree, which
is the finding the old count named.

Verified: clean build with the documented three warnings (the help bundle's `-Woverlength-strings`
is gone, so the baseline moves from four to three); test-ui 14/14; test-help clean (the four
subcommands and 15 pages rendered); a temp-prefix install lands 15 `.page` files and 3 figures at
`share/help/C/org.udpscope.app/`; the headless run reads `[help] topics=14 index=1 figures=3 dir=...`
with 0 Gtk-CRITICAL. Yelp's own window could not be exercised on this box -- it starts under Xvfb,
activates its portals and never maps, the same WebKit/portal limitation that stops a grab -- so its
rendering and its search index are unverified here; the content pipeline was (`yelp-build` renders
the whole set).

Docs: the manual's build/install/open story in `build-and-run.md` and README; the warning baseline
in `verification.md`/CLAUDE.md; the help bullets in `invariants-gtk-and-chrome.md`; the `[help]`
line in `probe-readings.md`.
2026-10-14 16:49:30 +02:00