14 Commits
Author SHA1 Message Date
Jean-Pierre MartineauandClaude Opus 5.5 11b5274b31 Docstrings and comments of the tests are in ASD-STE100
Each test docstring states the behaviour that the test proves, in the
simple present. No assertion message, test id, or other string literal
changed. Four statements now agree with the code: the Trace highlight
stays on the last row, a menu count, and two others.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 21:36:50 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 b6e7cd5c16 The handhelds are named for their variant: picocalc-lyra and cardputer-zero
M5Stack makes three Cardputers and only the Zero runs Linux; the PicoCalc
takes several cores and only the Luckfox Lyra runs MeshTerm. So the bare
names were ambiguous, and every reference to the two devices now carries
the variant: the --platform values and MESHTERM_PLATFORM, the Platform
constants (PICOCALC_LYRA, CARDPUTER_ZERO), their lane decks and every
screen's picocalc_lyra_lane / cardputer_zero_lane, the font inventories,
the lane chips' theme styles, the scripts/ folders, the two chrome test
files, and the docs and CLAUDE.md that name them.

There is no alias: --platform picocalc is refused with the three real
choices listed, rather than falling back to the desktop layout. The
PicoCalc is auto-detected from its device tree, so it needs nothing.
What names hardware or a user's data keeps its name: the PicoCalc
keyboard's input-device name, its battery's sysfs supply, and the serial
profile the Lyra setup script writes. .gitignore follows the firmware's
folder, and the changelog keeps the words each release used.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 11:52:41 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 c9f5a71ce7 Every Python file names its licence with an SPDX identifier
Apache-2.0 does not require per-file headers, but a file lifted out of the tree carried no marking at all. One comment line, first (or right after a shebang), on every .py under meshterm/, tests/, packaging/ and scripts/, plus the extensionless SPI bridge; tests/test_spdx.py keeps a new file from landing without one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 13:09:18 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 356ca6a94a The widgets CLAUDE.md names as THE widgets have public names
A leading underscore says "internal to this file, don't import me", and the
column-age formatter and the node glyph table wore one while seventeen modules
imported them and CLAUDE.md named them as the one place each concept lives.
format_age sits beside format_ago now (prose age, column age), NODE_GLYPHS and
DEFAULT_GLYPH beside them, age_seconds with the ages. A mechanical rename, 140
references across 24 files, CLAUDE.md's three lines with them so the guide and
the code move as one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 01:13:18 -04:00
Jean-Pierre MartineauandClaude Opus 5 61dc9dc110 Every icon-led list measures its icon column, so no label starts a column early
The terminal draws some icons in one cell (↻ ⌨ 🗑 ✓ ✗ ⚠) and most emoji in two, so
a row written icon-plus-space started its words a column left of its siblings. The
fix kept landing one screen at a time. menus.align_icons now pads every icon in a
list out to the widest one, and menu_rows applies it itself, so a list built there
cannot misalign.

Hand-built lists are brought along: the courier outbox and entry actions, the
watchtower actions and rule popover, the record card's actions, the main menu's
Quit row and the device splash's action rows. The repeater admin's regression
test, dropped by a concurrent rewrite of its file, is restored.

tests/test_icon_alignment.py sweeps every gallery screen on both platforms and
fails, naming the rows and their start cells, when a list's icon-led rows start
their words in different cells.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 02:02:05 -04:00
Jean-Pierre MartineauandClaude Opus 5 92d31a3191 Every file is spelled the way the formatter spells it
`ruff format .` across the repo: 153 files, no behaviour change. This is the
second gate CONTRIBUTING.md promises, and until now it reported 154 files needing
reformat — so a first contributor ran the command the guide told them to run and
could not tell their own diff from the background.

Landed on its own, with nothing else in it, because a whole-repo reformat is only
safe to skip past in blame if it is provably nothing but layout. It was checked
that way rather than assumed: every changed file was formatted in memory and
compared by full `ast.dump()`, and 152 of 153 come out structurally identical
(the one exception was a docstring opening on a quote character, fixed in the
commit before this one). The string and comment token streams were diffed
separately, so a quote flip or an edited comment could not hide inside a
re-layout — five quote-style normalisations and sixteen implicit-concatenation
joins, all value-identical, and one commented-out line gaining the space after
its `#`. `meshterm specimen` is byte-identical on both platforms, and the suite
is 1888 green, gallery gate and `test_theme16` included.

What actually changed, in order of how much of the diff it is: wrapped calls and
signatures rejoin onto one line where they fit inside the hundred columns; the
magic trailing comma expands what it is there to expand; complex slices gain
their spaces (`[len(noise) :]`); a few redundant parens go. The tables that are
laid out to be read were fenced in the previous commit and are untouched here.

The aligned trailing-comment column is gone from about ninety lanes that were not
fenced — including the `heat.*` ladder in `MESH_THEME`, which cannot be fenced
where it sits. Every comment stays on the entry it describes, so nothing there
reads wrong; it just no longer lines up.

Add this commit to `.git-blame-ignore-revs`, which follows immediately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 01:07:19 -04:00
Jean-Pierre MartineauandClaude Opus 5 40ecf5151d The tests' docstrings lead with a sentence too
The last 122 `D205` sites, which takes `ruff check .` to zero.

Same shape as the package pass and just as additive: these were a paragraph
with no summary, so each one gains a plain sentence saying what is being
asserted, and every word that was there moves down into the description. The
dense ones benefit most — `test_pathline`'s 23, where "A seam is one cell, not
two" now stands alone and the chevron mechanics explain it underneath, and
`test_tui`'s wide-glyph repaint test, whose seven lines on prompt_toolkit's
relative cursor now sit under the one line that says what the test pins.

Four module docstrings (`test_advert`, `test_gallery`, `test_platforms`,
`test_repeater_admin`) opened with "X tests: a, b, and c" and now name the
subject on the first line with the list below it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 20:37:51 -04:00
Jean-Pierre MartineauandClaude Opus 5 61b6387269 Every zip says whether its sides are the same length
`B905` over 56 sites, and the answer differs by idiom rather than by file.

21 were the sliding window `zip(xs, xs[1:])` — ragged by exactly one, on
purpose, so `strict=` is the wrong question entirely. Those become
`itertools.pairwise`, which is what they always meant and needs no flag: the
walk's SNR stops, the heat ladder, the route chains in `topology`/`records`,
every polyline in `mapcanvas`, and the graph's edge sets in `pathgraph`.

The other 35 pair sequences that must line up, so they now say `strict=True`
and a mismatch raises instead of silently truncating the longer side. Running
them under that assertion is what sorted the two groups: the first pass put
`strict=True` everywhere and 208 tests failed, all of them a sliding window.

One goes the other way. `mvt._decode_feature` reads tags off a network tile,
where an odd-length list means the tile is malformed — and this decoder answers
that with a shrug everywhere else (`if not geom_ints: return None`, bounds-
checked key and value indices). It keeps `strict=False`, now with a comment
saying why, so a bad basemap tile still can't crash the map.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 20:12:07 -04:00
Jean-Pierre MartineauandClaude Opus 5 71b16ec7b7 The annotations, imports, and dead locals answer to ruff
`ruff check --fix` over the whole tree, ahead of putting the linter in CI:
1977 mechanical fixes, overwhelmingly `Optional[X]` → `X | None` (1186 in the
package alone) and the quoted annotations that `from __future__ import
annotations` already made unnecessary, plus import sorting and unused imports.
Nothing here changes behaviour — the suite is 1885 green before and after.

Four findings were real rather than stylistic and are fixed by hand: a dead
`cells` range in `MapCanvas`, a dead slot dict in the heat-ladder test, a
`ProgressScreen` task handle nobody read, and a tab loop binding a `label` it
never used. The two `F821`s were quoted forward references to names imported
inside their function bodies — now declared under `TYPE_CHECKING`, which keeps
`test_performance`'s local import (it guards the startup import graph) while
letting the linter resolve the name.

412 findings remain and are deliberately untouched: they need a decision about
the rule, not an edit to the code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 17:26:12 -04:00
Jean-Pierre MartineauandClaude Opus 5 bcf387c620 The test fixtures stop being a picture of one person's mesh
Every node in the suite was named after hardware on JP's desk: YUL-Cartierville
and YUL-Poly are repeaters he runs, two more were the BLE
companions, another his own handle, and a long tail of YUL-Far / YUL-Me / bare
YUL besides. A stranger reading the tests learned his callsign, his city and his
gear before learning what the tests check.

They join the family the suite had already invented for exactly this — Alice,
Lounge, Yagi-Repeater, hub-1: Hilltop-Repeater, Lakeside, Waymarker, Homestead,
and Hub for the 3d-keyed node. Hub is not a new name; test_device_config already
called that contact Hub, so the suite was carrying two names for one fixture.

Replacements are the same length as what they replace, so every width budget,
truncation assertion and gallery line is unchanged. The two map tests that type
a literal query picked `yu`/`yul` to match the old name; they type `hi`/`hil`
now, same length, same single match.

Left alone on purpose: the About page and the donate link, where the handle and
the repeaters are the point, the hand-drawn splash art, and the XIAO README
reporting what JP's own radio advertises.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 02:01:55 -04:00
Jean-Pierre MartineauandClaude Opus 5 07481c4fd1 Watchtower: pin the alert, scroll the message
←→ was sliding the whole alert row, so reading a long message to its end cost
the marker, age, kind, glyph and node name off the left edge — the lanes that
say *which* alert this is, and the part that already fitted. Each row now
declares them (`Choice.hscroll_from`, the seam the Trophy case cut), keeping
the `—` with the head so the message still arrives introduced.

The ←→ footer atom is gated on the run rather than the whole label, so a row
whose pinned lanes make it wide but whose message fits no longer advertises a
key that would do nothing.

The section jumps were already there: two `section_heading` rows earn the
list its ^PgUp/^PgDn and their F1/F2 chips automatically.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 01:30:18 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 f164201ed6 Watchtower: lead an alert's node name with its type glyph
The watchlist rows and the star-a-node picker already mark each node with
its shared type glyph (▲ repeater, ● node, …); the alert log didn't. Add
it there too, immediately left of the node name — the leading ●/○ on an
alert is the acknowledgement state, so the type marker belongs with the
name, not the row head.

The type is resolved from the node's key: the contact table's advertised
type wins, a watched entry's stored type backfills (so an alert still gets
its glyph when the node has left the device's contacts), and an unknown
type falls back to the plain-node ● exactly as the watchlist rows do. An
acked alert mutes the glyph with the rest of its history.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 14:02:21 -04:00
Jean-Pierre MartineauandClaude Fable 5 c4622d529a Watchtower: type glyphs, key-hued names, sideways scroll
Watchlist rows now lead with the shared node-type marker in its own
colour (the type is stored at star time; legacy entries resolve
through the contact table) with the name in its key-derived hue —
silence keeps its ⚠ tail instead of tinting the glyph. Unacked alert
labels resolve back to a key for their hue (contacts first, then the
watchlist's own names, so a starred node colours even offline); acked
rows recede to muted whole. The star-a-node picker draws the same
glyph-and-hue rows. Long rows can now be read to the end: SelectScreen
grows an opt-in hscroll flag — ←/→ slide every row under the pinned
pointer via crop_cells, clamped at the widest row, surviving ↑↓ and
resetting on filter edits — enabled only here, with the footer
advertising it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 01:09:56 -04:00
Jean-Pierre MartineauandClaude Fable 5 b6fcda0f91 Watchtower: a passive sentinel with alerts for the nodes you care about
Star the nodes that matter (the roof repeater, the far gateway) and
session-long rules watch what the always-on hub already hears: a
silence alarm when a starred node goes quiet past its threshold
(latched per quiet spell, re-armed — with a recovery note — when it
returns), an SNR-sag warning when its receptions' median drops well
below the run before it, and a mesh-wide heads-up the first time a
never-before-seen node id appears (baselined against the whole stored
observation history, announced at most once ever). Nothing transmits.

Alerts persist in watchtower.json alongside the watchlist and rules,
show as a red ▲ badge in the header, and are read, acknowledged, and
configured in a new Mesh-section Watchtower screen. A remote-battery
rule was considered and dropped: observations carry no parsed battery
telemetry, so there is no honest data source for it yet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 05:35:36 -04:00