Commit Graph
14 Commits
Author SHA1 Message Date
vicrodh ecae778d58 fix(player): preserve audible state on source failures
Preflight fallible queue transitions and publish now-playing state only after the requested source is audible, leaving the prior stream intact when a file or server is unavailable.

Carry source-owned request headers through remote probes, streaming and fallback downloads without exposing sensitive values.

cx
2026-08-25 18:26:35 -06:00
vicrodh d244213a64 fix(library): harden cross-source catalog data
Preserve per-disc and collection artwork, multi-value genres and remote metadata across Local, Plex, Jellyfin and Subsonic.

Apply source, format and quality filters before pagination, reject invalid CUE rows, and keep Qt library surfaces aligned with the selected physical version.

cx
2026-08-25 18:26:06 -06:00
vicrodh d40fbb36d2 fix(source): a source word the caller supplied outranks a namespace bit
Four George Harrison albums would not play from a MyQBZ collection:

  ambiguous id "5099951336752" (Some(Album)):
  candidates [SourceId("qobuz"), SourceId("subsonic")];
  caller must supply a source

The caller HAD supplied one. The MyQBZ row stores `source = 'qobuz'` and
`RawRef::from_mixtape_item` threads it through correctly; nothing about
the storage or the call chain was wrong. The bug is that
`SubsonicSource::recognises` ORed a namespace-bit test ON TOP of the
source word, so it claimed an id it can never own while Qobuz claimed it
legitimately — two hits, and `claim` refuses to guess.

`5099951336752` is an EAN-13 barcode (EMI/Parlophone, "Somewhere In
England"). Checked arithmetically rather than assumed: bit 42 set,
nothing above the payload mask — exactly `RemoteSource::of_id`'s Subsonic
predicate. The reserved band is 4_398_046_511_104 ..= 5_497_558_138_879,
i.e. EVERY 13-digit barcode from 4398… to 5497…, which is all of
EMI/Parlophone's 5099… prefix. That is why four albums failed together
and why it is deterministic per album rather than flaky.

Two gates, applied to all three cached sources:

  - An explicit and DIFFERENT source word now returns false outright. A
    bit pattern must never outvote a caller who already said what this
    is.
  - A namespaced numeric is a TRACK rowid by construction, so the arm no
    longer fires in ALBUM position at all. That is the narrower and more
    honest statement, and on its own it would have spared this barcode.

Plex needed the same edit and not as tidying: its arm is the loosest of
the three (a bare bit test, no payload bound) and its Album branch
answers `Err(BadIdShape)`, which the registry propagates AHEAD of any
tiebreak — so a barcode with bit 40 set failed with a confusing *Plex*
error on an id Plex never owned.

WHY THIS WAS NEVER CAUGHT: every disjointness test used the barcode
"0060254702523", whose LEADING ZERO puts it below all three floors, so
the entire in-band class was untested. The new test uses the real id and
asserts up front that it still sits in the band, so it cannot quietly
stop covering the case. Verified the honest way — with the old predicate
restored the new test FAILS, with the fix it passes; 90 tests green, and
`namespace_bit_is_enough_to_recognise_without_a_source_word` (the Plex
mixtape-track case that must not regress) stays green.

cc
2026-08-22 04:17:57 -06:00
vicrodh 452589ddaa feat(qt): open a SACD image and play it — the disc names itself
`Open > Open SACD image…` picks a .iso, reads its stereo area and publishes it
as the ephemeral session. Nothing lands in library.db.

Unlike a CD, a SACD needs no network to be named: the Master TOC carries the
album title and artist, and the area TOC a title per track. The owner's disc
opens as "Wagner: Das Rheingold", 24 tracks, every one with its German title
and "Berliner Philharmoniker, Herbert von Karajan, etc." — all of it read off
the image.

Routing is one line in `LocalSource::playback` and none in the player: a
`sacd:` reference rides the existing DsdFile ticket, because `open_dsd` is the
funnel all six player-side DSD constructions already go through. Teaching it
there is what makes the PCM, DoP, native and gapless paths inherit disc images
for free.

Driven end to end on the owner's Rheingold: the tab reads "Wagner: Das
Rheingold 24", the badges read HI-RES / DSD64, and pressing play took the DoP
route —

  Player: DSD track — DSD64 via DoP (176400 Hz carrier)
  [ALSA Direct] DoP hardware configured: 176400Hz, 2ch, S32_LE
  Audio thread: DoP playback STARTED

which is the outcome that matters: the disc's DSD bits reach the DAC without
being converted to PCM at all.

Six new strings in all eight locales. Unsupported cases are named rather than
approximated — a multichannel-only area, a DST-compressed one and a file that
is not an image each get their own message.

cc
2026-08-21 01:34:23 -06:00
vicrodh 3ba476b43c feat(source): a CD track is a ticket of its own, not a file
`PlaybackTicket::CdTrack { device, start_lsn, sectors }`. No path-shaped
variant fits: a track on a disc is a device plus a sector RANGE, and the bytes
only exist while that medium is in that drive. `File` and `DsdFile` would both
hand it to something that opens it.

`LocalSource::playback` answers it FIRST, before the extension tests, through
`CdRef::is_cd_path` — the encoder's own predicate rather than a second
`starts_with` written out at the call site. That is deliberate: a hand-copied
prefix list is exactly how six bugs of the same shape shipped on 2026-08-20,
and a fold to a valid-looking answer is invisible to every test.

The match in `audible_qt::play_ticket` is exhaustive on purpose, so the new
variant broke the build until somebody decided what a CD means. It now means:
read the sectors and feed them as 44.1 kHz / 16-bit / stereo PCM.

`EphemeralLibraryState::open_tracks` installs a track list that did not come
from scanning a directory. `open_folder` cannot serve a disc — it takes a
`&Path`, rejects anything that is not a real directory, and derives every
field by reading files off a filesystem. A disc has no filesystem to read.
Ids come from the same synthetic range, so every playback caller keeps routing
them without knowing a disc exists.

cc
2026-08-20 23:29:00 -06:00
vicrodh 8c3514b899 fix(qt): play a downloaded album from Local Library through the offline tier
A CMAF download's `local_tracks.file_path` is the BUNDLE DIRECTORY
(`tracks-cmaf/<qobuz_id>/`, holding `init.mp4` and an encrypted
`segments.bin`), not a playable container. `LocalSource::playback` handed
it back as a `PlaybackTicket::File`, the read failed with EISDIR, and the
audible step reported it as

  audible: file not available at …/tracks-cmaf/266725026 (drive unmounted?)

— a message about a drive, for a bundle sitting on the local disk. The bar
adopted the track anyway, so the album looked like it was playing and no
audio came out.

Three funnels reach the audible step and only two excluded an offline row:
`play_current_if_local` (auto-advance) and
`playback_qt::play_resolved_offline_aware`. The Local Library PLAY funnel
did not, nor did its hand-copy in `local_album_actions`. Only qbz-core's
offline tier can decrypt the bundle, and the row carries the real Qobuz
catalog id precisely so that tier can be asked for it.

- `local_playback::play_audible` is now the ONLY copy of the audible step
  and routes an offline row to the offline tier; `local_album_actions`
  drops its copy and calls it.
- `LocalSource::playback` refuses an offline row with `BadIdShape` instead
  of emitting a ticket that lies. The test is on the row data, never
  `is_dir()`: a stat there would block for the mount timeout on the
  unreachable share the audible step's bounded probe exists to survive.
- Tests run the row through the real `local_queue_track`, because the two
  halves that must agree are its source word and the badge the router reads.

Verified by driving the window: album play, track-row play and skip-next all
log `[Core] track ... served from OFFLINE cache` and decode at 44.1 kHz /
24-bit; a plain user-file album still plays through the local audible step at
96 kHz / 24-bit. qbz-qt 431/431, qbz-source 91/91.

cc
2026-08-20 18:16:33 -06:00
vicrodh 63a4f2daf1 feat(qt): the Local Library union, the settings store and the sweep
Three pieces that only mean anything together: rows in the shared cache,
a union that shows them, and settings that decide whether either
happens.

THE UNION. `get_albums_metadata_page` grows a second, INDEPENDENT
attachment. `plex_cache` stays; `remote_cache` is the shared mirror, and
they attach separately on purpose — a missing Plex cache must not cost
the user their Jellyfin rows. `attach_best_effort` is extracted because
the failure mode of a botched ATTACH is a silently local-only library,
and a failure stays non-fatal: refusing to list any albums because one
server is off turns "Jellyfin is unplugged" into "your library is
empty".

The union takes an ENABLED-SOURCES list, not just a path. The mirror
holds every remote source, so the master toggle has to be a filter
rather than a detach: turning Jellyfin off must hide Jellyfin's rows and
leave Subsonic's alone. That predicate is interpolated into SQL, so it
is VALIDATED rather than escaped — only [a-z] survives, an all-invalid
list yields `0` (show nothing, the safe direction), and a test feeds it
`x'; DROP TABLE local_tracks; --`.

The remote group key is `<source>:<album id>`, matching Plex's
`plex:<hash>`. That namespaces two servers using the same album id AND
lets a source claim an album card that arrives with no source word,
which is every card that round-trips through a QML string property —
`recognises` grew that arm and `claim` strips the prefix, because
carrying it inward is how Plex ended up with two album keys only one of
its queries understood (IC-2).

THE SETTINGS. One store for both servers, keyed by a `server` column,
mirroring the shared-cache decision. What each protocol needs differs
and `is_configured` is the one place that knows: Jellyfin needs its
issued token, Subsonic needs a password because it has no session and
re-derives `md5(password + salt)` per request.

Two identifiers are minted once and persisted, and both are
load-bearing. Jellyfin's DeviceId, because re-authenticating under the
same one revokes the previous token — a constant would make two installs
log each other out, a fresh one per launch drops the last run's token.
Subsonic's salt, because rolling it per request makes every cover URL
unique and the artwork cache keys on the URL.

THE SWEEP. In the frontend, where the runtime and the progress UI are,
writing through the cache handle the SOURCE owns so sweep and reads can
never point at different users. Jellyfin gets progress and a delta path
(45.8 s for 4924 rows); Subsonic needs neither (0.81 s for 6678).
`prune_stale` runs only after a COMPLETED full sweep — a delta never
prunes, because it deliberately does not see unchanged rows and would
read every one of them as deleted. `last_sync_at` is stamped only on
success for the same reason.

Verified against the running binary, twice:

  source registry bound: local_tracks=617 plex=connected
    ephemeral_store=yes remote=none configured
  jellyfin: minted a device id for this install
  subsonic: minted a salt for this install

...and on the second launch, zero re-mints and the same values on disk —
which is the property that keeps a Jellyfin session alive.

qbz-qt 428/428, qbz-app 224/224, qbz-library 100/100, qbz-source 88/88.
Release build clean, zero warnings.

cc
2026-08-20 09:20:07 -06:00
vicrodh 51aa8ba081 test(source): walk the whole chain against both real servers
The unit tests prove each method in isolation against a hand-made row.
They cannot prove the thing that actually broke this project once: that
the pieces are CONNECTED. `source_wiring` shipped a registry whose
sockets were empty and every test stayed green, because a pure-function
test has no bound user and a structural gate only proves the OLD path is
gone.

So this walks it: sweep a real page off the server, write it through the
same mapping the sync will use, bind the source exactly as the app binds
it, then claim -> tracks -> meta -> artwork -> playback, and finish by
pulling real bytes through the url the source produced. Nothing is
asserted from a type; everything is asserted from a server's answer.

The assertion with the most teeth is the identity round trip. A queue
row as `cached_to_queue_track` builds it — namespaced id plus the
server's id in the hint — must claim back to the server's id, and the
same row must ALSO resolve with no hint at all, through the cache. Those
are the two shapes a producer can hand over, and a mismatch between them
is bug 2 wearing a new source's clothes.

`assert_serves_audio` issues a Range request, which is what the
progressive feeder does: it checks seekability, refuses a chunked
(transcoding) answer, runs the Subsonic error-envelope guard over the
body, and keeps the test to 64 KB instead of a 100 MB FLAC.

Against the bench, 3/3:

  jellyfin  swept 500 of 4924 -> album "...And Justice For All",
            9 tracks, tier "hires", cover image/jpeg (tokenless,
            self-keying), stream 206 audio/flac
  subsonic  navidrome openSubsonic=true, swept 500 -> album
            "POWERS OF VOICE (Disc 1)", 13 tracks, tier "cd",
            cover image/webp keyed on subsonic:al-…_0 rather than on
            the credentialed url, stream 206 audio/flac
  registry  each namespace routed to its owner, and a Plex id still
            goes to Plex

The third needs no server and runs everywhere.

cc
2026-08-20 08:01:47 -06:00
vicrodh b09f03b739 feat(source): JellyfinSource and SubsonicSource — §10's acceptance test, paid
Design 02 §10 predicted a fourth source would be "one impl plus one
registration line, zero edits in any view". Two of them landed, and the
prediction held: no arm in any view, none in the playback router, none
in the artwork pipeline, no entry in a url taxonomy. Every one of those
became structural during stages 3 and 4, which is what this commit is
the receipt for.

One honest correction to that claim, now written in sources/mod.rs: it
was true only AFTER stages 3 and 4. From stage 1 the seam existed but
playback and artwork never asked it anything, so a source added earlier
would have claimed correctly and then been silent and coverless.

`sources/remote.rs` holds what the pair shares — the cache handle and
the row mappers — so they cannot drift the way the three copies of
play_audible did. The two conversions that are easy to get backwards
(ms -> s, Hz -> kHz) live there once, with a test.

Identity, and it is the part with teeth. A queue row carries BOTH the
namespaced id the grid published and the server's own id in the hint,
and `claim` prefers the hint: rowids are reassigned by a cache rebuild,
the server's id is not, so a queue that outlives a re-sync still
resolves. Recognition is by source word or namespace bit ONLY — never by
the shape of an opaque id. A Jellyfin item id is a 32-char hex GUID and
a Subsonic one is base62, but neither is guaranteed, and inferring a
source from a string's alphabet is the class of guess the seam deletes.
Both sources are tested against every neighbouring floor: Plex's 1<<40,
each other's, the ephemeral store's 1<<48, and a plain local rowid.

The Subsonic brand spellings — navidrome, gonic, airsonic, astiga — fold
to ONE source. They speak one API and QBZ reads them through one impl,
so a row stamped with the server's brand must still resolve; returning
None would make it Unclaimed and it would refuse to play.

Artwork differs between the two, and only because the protocols do.
Jellyfin's cover url carries no credentials, so it is stable and its
cache_key can be the url itself. Subsonic's carries the salt and the
size, so its key is the opaque coverArt id instead — keying on the url
would re-download every cover whenever either moved, and would put a
credential in a cache key.

Both playback tickets are Stream with the bit-perfect contract spelled
out: `?static=true` for Jellyfin, `format=raw` for Subsonic, and tests
that fail if `audioCodec` or `maxBitRate` ever appear.

One test had to be rewritten rather than deleted: `from_word("jellyfin")
== None` was the example for "unknown words are not guessed into Qobuz".
The property still holds and is now spelled with "emby".

qbz-source 84/84, qbz-media-cache 11/11, qbz-qt still builds clean.

cc
2026-08-20 07:58:03 -06:00
vicrodh 604f336e24 fix(qt): wire the source registry to its stores — it never was
This is the regression report from the previous build, and the cause is
that stage 1 built three injection sockets and nothing ever plugged into
them. `set_creds`, `set_ephemeral` and `bind_user` had zero call sites
in the entire frontend.

It stayed invisible for two stages because nothing needed them:
`claim` is pure, and `PlexSource::tracks`/`meta` read plex_cache.db
through qbz-plex's own global. The first consumers that DO need them are
playback and artwork — stages 3 and 4 — so routing those onto the seam
turned a latent gap into a live break:

- every Plex row: "plex: not configured (no Plex credentials
  configured)", so Plex playback died and every Plex cover in the Local
  Library grid resolved to ArtRef::Unavailable and vanished;
- every LOCAL row: an unbound DbPool refuses all reads, so LocalSource
  found no track and local playback died too;
- ephemeral rows had no session store to resolve against.

Wired now, each to the ONE authority that already existed: PlexCreds
over local_plex's settings store (read per call, never cached, so a
connect/disconnect mid-session is picked up), EphemeralTracks over the
session store, bind_user as the first statement of
bind_per_user_stores, teardown on logout, and the unbound-window
fallback so pre-login reads resolve the way local_state::with_db already
resolves them.

Also wired: the album identity mode. The grouping IS the query, so a
metadata-mode user's album play would have resolved a different track
list than the grid rendered.

`report_wiring` logs one line at bind time asking each socket a question
only a wired one can answer. The failure this fixes is silent by
construction — an unbound pool and a disabled Plex look identical to a
user with no local tracks and Plex switched off — and it stayed hidden
because the tests are pure-function tests and the structural gate only
proves the OLD path is gone, never that the new one is connected.

Verified against the running binary, not the test suite:
  source registry bound: local_tracks=617 plex=connected
  ephemeral_store=yes

qbz-qt 423/423, qbz-source 59/59.

cc
2026-08-20 02:12:17 -06:00
vicrodh ffb35085c9 feat(source): each source interprets its own raw artwork token
Design 02 §9 stage 4 needs the art index to carry an ArtRef instead of a
String, and the producers of that index are the row mappers, which hold
no MediaRef — the windowed grid pipeline is keyed by art_key (§3.4). The
two ways to give them one were both bad: sniff the string later (that is
`artwork_qt::classify`, i.e. bug 3), or claim per visible row and let
the source look the token up again, which for Plex is one plex_cache.db
query per row on a cold grid — on the exact grid whose scroll cost is
the open perf item.

`Source::artwork_token` is the artwork analogue of `claim`: the source
says what its OWN token means, at the moment provenance is still known.
It is pure — no network, no DB, no filesystem — because the token is the
whole input.

This is what makes stage 5 free. `classify` recognises a Plex thumb only
because somebody bolted `local_plex::is_thumb_path` onto it, so a
Jellyfin token (`/Items/<id>/Images/Primary`) reads as a filesystem path
— it starts with `/` — and the pipeline opens it from disk: blank cover,
no error, nowhere to look. Dispatching by owner removes the guess.

Three impls, each carrying the arm of `classify` that was really about
it: Local (file:// and absolute paths, plus the CDN url an offline row
can keep), Plex (the two declared thumb prefixes, Unavailable rather
than None when there are no credentials), Qobuz (CDN url, cache_key on
the RAW url so downscaling does not re-download per size class).

An unregistered source returns None, not Unavailable: a row whose source
word this build does not know has no art to wait for, and Unavailable
would park it in the retry-when-connected bucket forever.

59/59.

cc
2026-08-20 01:35:47 -06:00
vicrodh 7113ef5794 feat(source): add PlaybackTicket::Stream and emit it from PlexSource
Design 02 §3.3 predicted a `Stream` variant "when the documented
progressive-streaming follow-up lands". It landed first:
`local_playback::play_plex_track` already resolves the part URL and
Range-streams it through `qbz_player::remote_stream`, keeping the
whole-file download only as the fallback for a server that refuses
Range. Stage 3 cannot route playback through the ticket while the
ticket has no way to say "stream this".

`PlexSource::playback` now calls `plex_resolve_part_url` instead of
`plex_resolve_track_media`: the ticket resolves a LOCATION, it does not
fetch a body. Dropping the old `resolve_track_media` fallback is not a
behaviour loss — both functions run the same metadata request, the same
parse and the same part-key extraction, and only then does the second
also GET the body, so every way the first can fail the second fails
identically. Carrying the url on the ticket also removes the duplicate
metadata round trip the fallback performed.

`duration_secs` comes from the queue row rather than the core's current
track: a caller may be priming a row that is not current yet, where the
core would answer about a different one.

The url embeds credentials (Plex's token, and Jellyfin's / Subsonic's
when they arrive), so `Debug` redacts the query string while keeping the
origin — telling a LAN address from a plex.tv relay is exactly what the
perf log lines exist to answer. Three tests cover the redaction and the
`play_id()` arm.

cc
2026-08-20 01:10:30 -06:00
vicrodh b6cf06ff2a feat(source): wire My QBZ through the seam, and land the owner's container rules
Stage 2. The measured win: opening a 34-item collection now costs **2** library.db
opens instead of 23-34 — the local source owns a pooled handle and hands out no
guard, only `with(|db| ...)`, so a `!Send` `&LibraryDatabase` cannot cross an
await either. Bug 1 from the seam design is dead by construction, not by care.

The owner's rules, decided today, now live in one place instead of eight:
- **Collection** takes albums, singles and EPs from ANY source — qobuz, folders,
  Plex, and whatever is added next. **Mixtape** takes albums, playlists and
  tracks. This was a single boolean (`restrict_to_mixtape`) that could express
  neither release types nor "collections accept local".
- An unsupported combination must never be VISIBLE: the picker stops offering
  the impossible action, and the error stays a log-only safety net. Owner: "el
  bloqueo no debería ser visible nunca, más bien esto hay que ajustarlo en la UI".
- **Ephemeral honours its name** — playable, queueable, and offerable to nothing
  else.
- A row's source comes from the ROW. Eight QML sites built the add-to-mixtape
  payload with `"source": "qobuz"` written as a literal, so a Plex or local
  track was stored as Qobuz and afterwards could not play at all: the app asked
  the Qobuz API for an id that never existed there.

`QobuzSource` reads the client through a lens on every call instead of holding a
clone. That is not style: `qbz-core` REPLACES its client (core.rs:346 and :384,
both on paths that run before `set_session`), so a cached clone goes stale and
fails silently — the hazard the crate's own doc comment described.

The lens is installed in `on_boot`, first of everything, and finding out why is
the lesson of this commit. The gate was green — build, 142 tests, both QML
audits, a clean offscreen smoke — and the feature was visibly broken: every
Qobuz row showed "No results found", an empty Quality cell and a wrong "SINGLE"
type, while the Plex rows resolved fine. The registry is a OnceLock, so whoever
touches it first BUILDS it, and one built without the lens leaves Qobuz
permanently detached. Only driving the real app surfaced it; the crate's error
string named the cause exactly, which is the one thing that made it a ten-minute
fix instead of an afternoon.

cc
2026-07-30 17:14:40 -06:00
vicrodh f1b4241342 feat(source): add qbz-source, the source-agnostic media seam
Stage 1 of the seam the owner asked for: "son vistas súper híbridas donde se
mezcla Qobuz/Local(Folders/Plex) ... hay que hacer una solución agnóstica
porque no tardamos en meter a Jellyfin y quizás Navidrome". Nothing is wired
yet — this lands the crate, its three source implementations and its tests.
`qbz-qt` and `qbz` are untouched, so this commit changes no behaviour.

The problem it exists to end, measured rather than assumed: 129 source-branching
sites across 30 files in qbz-qt alone, SEVEN distinct item->tracks resolvers
none of which can reach the others, FIVE non-interchangeable artwork pipelines
over one correct taxonomy, and six incompatible source vocabularies — three of
which cannot express Plex at all, including the PERSISTED one (`AlbumSource` is
`Qobuz|Local`). Every hybrid view re-wires the per-source handling by hand,
which is why something is always missing.

One `Source` trait covering the four concerns together — tracks, metadata and
origin, artwork, playback — because splitting them is exactly what lets a view
forget one. A `SourceRegistry` with claim-priority ordering (Qobuz, Plex, then
Local; Plex-before-Local is load-bearing). `RegistryResolver` implements
`qbz_mixtape::ItemResolver`, so that crate and the Slint glue stay byte-
untouched and keep compiling.

The three measured bugs die structurally, not by discipline:
- The local DB was reopened PER CALL, so a My QBZ collection logged one
  "Opening library database" per item (34 measured live). `LocalSource` owns a
  pooled handle, and it hands out no guard — only `with(|db| ...)` — so a
  `!Send` `&LibraryDatabase` provably cannot cross an await. `qbz-mixtape`'s
  sync-resolver constraint becomes an API property instead of a comment.
- A `plex:<hash>` album key reached an API that wants a rating key. `MediaRef`
  has no public constructor: only `registry.claim` mints one, and
  `PlexSource::claim` rejects that shape in track position. The survey also
  found a second, undiscovered face of the same bug — "Go to album" on a Plex
  track row opens an empty page.
- Local rows in a hybrid list rendered placeholder discs. Traced to its real
  mechanism: those items are stored with `artwork_url: None` and the only
  backfill is behind a Qobuz-client gate, so offline or pre-login the art is on
  disk and never shown. `Source::artwork` is one call per row whatever the
  source, and keeps the cheap/cold split the local path already had.

Adding Jellyfin is one new file, four diff lines inside this crate, and one
line in `auth_qt.rs` — and the design states this only holds BECAUSE artwork
sits on the trait.

36 tests. The id tests derive their fixtures from `PLEX_TRACK_ID_FLOOR` instead
of transcribing them, which is not pedantry: the survey's own id table — the
artefact meant to be the definitive reference — paired a namespaced id with the
wrong rating key, and the transcribed test caught it on the first run.

Design and adversarial review (6 BLOCKER / 10 MAJOR / 7 MINOR, all corrected):
qbz-nix-docs/qt-frontend/2026-07-30-source-seam/.

cc
2026-07-30 16:14:30 -06:00