the banner, the apply, the review hop, ICON_AUTOTAG
The three Direction points
1. A backend query cheap enough to call on page open. It costs no
MusicBrainz request at all. Everything MatchForAlbum needs is already
on disk: tagging_items carries the top score and release from the
background prefetch, and tagging_candidates durably holds the scored
list. A folder nobody has scored yet answers "nothing" rather than
scoring it now — the limiters here are shared with every page the user
can open, and a page that quietly spends a minute of somebody's
MusicBrainz budget to draw a banner is worse than a page that says
nothing.
2. The banner, beside catalog-scope-notice where the issue
suggested, with "Apply tags" and "Review in Autotag".
3. The threshold defined once.autotag.ConfidentTier — because #90 asks for exactly this: the album page and strict auto-accept must
agree on what "high confidence" means rather than computing it twice. A
page that claims confidence the auto-accept pass would decline is the
app contradicting itself. What they do not share is written down
beside it: surfacing is a suggestion behind a confirm dialog,
auto-accept is an irreversible rewrite gated on further conditions the
tier cannot express, so this is the floor both stand on rather than the
whole of either test.
Four decisions worth the review
The tier is computed, not read.tagging_items.score is the raw
number; Recommend is what turns it into a claim, capping it for an
ambiguous runner-up, an incomplete alignment, or a folder too small to
corroborate itself. Filtering on the stored score would promise
confidence the scorer had explicitly withheld — pinned by the two-track
test, which scores 0.99 and is correctly not surfaced.
Applying is offered only where it would do the whole album. A
tagging group is a folder, so a multi-disc album is several. One button
applying to the best-scoring group would leave the album holding a mix
of old and new tags, which is the case the app's Blocking notification
level exists for. groupCount is the test; the answer there is review.
Nothing is said about an album the user already answered for. confirmed covers both a finished apply and an explicit "leave as is",
and arguing with the second would be actively wrong.
The join is audio_files.group_key, not a path-derived key. A
group carved out of a mixed-bag folder is keyed on its tags, so a
path-derived key finds nothing for exactly the messiest libraries this
is meant to help.
A bug it found
confirm-dialog is one singleton for every confirmation in the app,
and wa-dialog reports its close asynchronously — open = false
starts an animation and wa-hide arrives after it. So a hide belonging
to a question already answered lands after the next question has
opened and cancels it: the user is asked something, the dialog vanishes
on its own, and the call site is told they said no.
Found by writing two confirmAction() tests in one file — the second
could not be accepted at all. Reaching it in the app needs two
confirmations close together, which "Apply tags" makes possible. Each
ask carries an id now; only the wa-hide handler needs one, because
only wa-hide can arrive late.
One vocabulary addition
ICON_AUTOTAG, on ICON_PLAYLIST's rule — an icon names the noun it
acts on, so a suggestion pointing at Autotag wears the Autotag
destination's own mark. It was written inline in the sidebar; two call
sites is where a name stops being one component's detail, so icon-language.test.ts governs it now.
Verification actually run
make lint # 0 issues, all three tag sets
make test # all three passes green
make ui-test # 898 passed (79 files), up from 883
make e2e # 114 passed, chromium, against a seeded app
make bindings-check # current
make skill-check # 47 targets
frontend/node_modules/.bin/tsc --noEmit # clean
16 new specs across three files. Names go through page.getByRole(...).
And it was driven in the running app, with a confident match staged
into a real database through /__test/sql: MatchForAlbum answered strong end to end, the banner rendered, the confirm dialog's impact
line read correctly, and "Review in Autotag" landed on that group key
with the request attribute consumed (hasAttribute('group-key') === false) — which is the half no component test can see, since Autotag is
a cached primary view and the router is index.ts.
This leaves ConfidentTier in place as the shared floor, and MatchForAlbum as a worked example of reading the durable candidate
cache without touching the network — which is the same constraint #90's
auto-accept rule states ("uses already-cached candidate data — no
additional MusicBrainz calls").
Releases are manual (#115), so merging cuts no version.
Closes #28 — the last item in #73's Phase 1.
## Commits
| commit | issue | what |
| --- | --- | --- |
| `21b303b` fix(ui): stop a closing dialog answering the next question | #28 | a late `wa-hide` cancelled the *next* `confirmAction()` |
| `fe67849` feat(autotag): name the confidence tier two features have to share | #28, #90 | `ConfidentTier` / `Confident()` |
| `9118c16` feat(autotag): answer whether an album has a confident match | #28 | `MatchForAlbum(albumID)` |
| `b5d70ac` feat(explore): offer the autotag match on the album page | #28 | the banner, the apply, the review hop, `ICON_AUTOTAG` |
## The three Direction points
**1. A backend query cheap enough to call on page open.** It costs *no*
MusicBrainz request at all. Everything `MatchForAlbum` needs is already
on disk: `tagging_items` carries the top score and release from the
background prefetch, and `tagging_candidates` durably holds the scored
list. A folder nobody has scored yet answers "nothing" rather than
scoring it now — the limiters here are shared with every page the user
can open, and a page that quietly spends a minute of somebody's
MusicBrainz budget to draw a banner is worse than a page that says
nothing.
**2. The banner**, beside `catalog-scope-notice` where the issue
suggested, with "Apply tags" and "Review in Autotag".
**3. The threshold defined once.** `autotag.ConfidentTier` — because
#90 asks for exactly this: the album page and strict auto-accept must
agree on what "high confidence" means rather than computing it twice. A
page that claims confidence the auto-accept pass would decline is the
app contradicting itself. What they do *not* share is written down
beside it: surfacing is a suggestion behind a confirm dialog,
auto-accept is an irreversible rewrite gated on further conditions the
tier cannot express, so this is the floor both stand on rather than the
whole of either test.
## Four decisions worth the review
**The tier is computed, not read.** `tagging_items.score` is the raw
number; `Recommend` is what turns it into a claim, capping it for an
ambiguous runner-up, an incomplete alignment, or a folder too small to
corroborate itself. Filtering on the stored score would promise
confidence the scorer had explicitly withheld — pinned by the two-track
test, which scores 0.99 and is correctly not surfaced.
**Applying is offered only where it would do the whole album.** A
tagging group is a folder, so a multi-disc album is several. One button
applying to the best-scoring group would leave the album holding a mix
of old and new tags, which is the case the app's Blocking notification
level exists for. `groupCount` is the test; the answer there is review.
**Nothing is said about an album the user already answered for.**
`confirmed` covers both a finished apply and an explicit "leave as is",
and arguing with the second would be actively wrong.
**The join is `audio_files.group_key`, not a path-derived key.** A
group carved out of a mixed-bag folder is keyed on its tags, so a
path-derived key finds nothing for exactly the messiest libraries this
is meant to help.
## A bug it found
`confirm-dialog` is one singleton for every confirmation in the app,
and `wa-dialog` reports its close **asynchronously** — `open = false`
starts an animation and `wa-hide` arrives after it. So a hide belonging
to a question already answered lands after the *next* question has
opened and cancels it: the user is asked something, the dialog vanishes
on its own, and the call site is told they said no.
Found by writing two `confirmAction()` tests in one file — the second
could not be accepted at all. Reaching it in the app needs two
confirmations close together, which "Apply tags" makes possible. Each
ask carries an id now; only the `wa-hide` handler needs one, because
only `wa-hide` can arrive late.
## One vocabulary addition
`ICON_AUTOTAG`, on `ICON_PLAYLIST`'s rule — an icon names the noun it
acts on, so a suggestion pointing at Autotag wears the Autotag
destination's own mark. It was written inline in the sidebar; two call
sites is where a name stops being one component's detail, so
`icon-language.test.ts` governs it now.
## Verification actually run
```
make lint # 0 issues, all three tag sets
make test # all three passes green
make ui-test # 898 passed (79 files), up from 883
make e2e # 114 passed, chromium, against a seeded app
make bindings-check # current
make skill-check # 47 targets
frontend/node_modules/.bin/tsc --noEmit # clean
```
16 new specs across three files. Names go through `page.getByRole(...)`.
**And it was driven in the running app**, with a confident match staged
into a real database through `/__test/sql`: `MatchForAlbum` answered
`strong` end to end, the banner rendered, the confirm dialog's impact
line read correctly, and "Review in Autotag" landed on *that* group key
with the request attribute consumed (`hasAttribute('group-key') ===
false`) — which is the half no component test can see, since Autotag is
a cached primary view and the router is `index.ts`.
## Note for #90
This leaves `ConfidentTier` in place as the shared floor, and
`MatchForAlbum` as a worked example of reading the durable candidate
cache without touching the network — which is the same constraint #90's
auto-accept rule states ("uses already-cached candidate data — no
additional MusicBrainz calls").
Releases are manual (#115), so merging cuts no version.
Closes #28
`confirm-dialog` is one singleton for every confirmation in the app,
and `wa-dialog` reports its close asynchronously: `open = false` starts
an animation and `wa-hide` arrives after it. So a hide belonging to a
question already answered can land after the *next* question has
opened, and cancel it — the user is asked something, the dialog
vanishes on its own, and the call site is told they said no.
Each ask now carries an id. `close` ignores an id that no longer names
the question on screen, the button handlers pass none (they always mean
the current one), and only the `wa-hide` handler carries one, because
only `wa-hide` can arrive late.
Found by writing two `confirmAction()` tests in one file: the second
could not be accepted at all, because the first one's hide had
cancelled it before the click landed. Reaching it in the app needs two
confirmations close together, which the album page's "Apply tags" makes
possible.
`ConfidentTier` and `Confident()` are a name for what was about to be
written as `== RecommendationStrong` at two call sites: the album page
telling the user unprompted that there is a match for what they are
looking at (#28), and strict auto-accept rewriting files without asking
(#90). A page that claims confidence the auto-accept pass would decline
is the app contradicting itself, and #90 asks for exactly this — that
the two agree on what "high confidence" means rather than computing it
twice.
What they do not share is written down beside it. Surfacing a match is
a suggestion with a confirm dialog behind it; auto-accept is an
irreversible on-disk rewrite gated on further conditions the tier
cannot express — exact track count, every title matching, lengths
within a couple of seconds, no cover replacement, no MBID conflict. So
this is the floor both stand on, not the whole of either test.
`Confident` is a rank comparison rather than an equality, so a tier
added above "strong" later does not silently stop qualifying.
`MatchForAlbum(albumID)` is the question the album detail page needs to
ask on open: does the autotagger already have something confident to
say about this album, and what would applying it do.
**It costs no MusicBrainz request.** Everything it needs is on disk —
`tagging_items` carries the top score and release from the background
prefetch, `tagging_candidates` durably holds the scored list. The rate
limiters here are shared with every page the user can open, so a lookup
that fires on page load must not join that queue; a folder nobody has
scored yet answers "nothing", rather than scoring it now.
**The tier is computed, not read.** `tagging_items.score` is the raw
number and `Recommend` is what turns it into a claim, capping it for an
ambiguous runner-up, an incomplete alignment or a folder too small to
corroborate itself. Filtering on the stored score would promise
confidence the scorer had explicitly withheld — which the two-track
test pins.
**Nothing is said about an album the user has already answered for.**
Only a `pending` group qualifies: `confirmed` covers both a finished
apply and an explicit "leave as is", and arguing with the second would
be actively wrong.
The join is `audio_files.group_key`, not a key derived from the folder
path, because a group carved out of a mixed-bag folder is keyed on its
tags — so a path-derived key would find nothing for exactly the
messiest libraries this helps. `GroupCount` is returned because a
multi-disc album is one group per disc: a caller that applied to "the
album" from a single button would retag one disc of three.
The complaint was having to notice the metadata was missing, then go
and hunt the album down on the Autotag page. The album page now says it
while you are looking at the thing: "MusicBrainz has a match for this
album: <release> by <artist>", with Apply tags and Review in Autotag.
Four things about it are load-bearing.
**Applying is offered only where it would do the whole album.** A
tagging group is a folder, so a multi-disc album is several, and one
button that applied to the best-scoring group would leave the album
holding a mix of old and new tags — the exact case the app's Blocking
notification level exists for. `groupCount` is the test, and the answer
there is review rather than apply.
**It rewrites files, so it asks.** `confirmAction()` with an impact
line that says it cannot be undone and that nothing is moved or
deleted, because "rewrites your files" reads worse than it is. The
apply goes through `ApplyAsync`, the registered-job path, so progress
belongs to the jobs indicator and this page does not grow a second one
— what it owes the user is the acknowledgement, because the button is
here. The suggestion clears itself on success rather than inviting a
second click while the job runs.
**The banner does not quote a percentage.** The backend has a score and
deliberately keeps it out of the sentence: 0.95 reads as a probability
and is not one. Which release it is, is the part a person can judge.
**"Review in Autotag" lands on that album.** The queue is sorted by
score so the intended folder is often near the top, and "often" is a
link that sometimes opens a different album. Autotag is a cached
primary view, so there is no construction to hand a payload to: the
request goes on as an attribute and the view *consumes* it, or every
later visit would reopen a folder the user finished with long ago.
`ICON_AUTOTAG` joins the vocabulary at the same time, on the rule
`ICON_PLAYLIST` was chosen by — an icon names the noun it acts on, so a
suggestion pointing at Autotag wears the Autotag destination's own
mark. It was written inline in the sidebar; two call sites is where a
name stops being one component's detail, so the sweep governs it now.
Verified against the running app with a staged match: the banner, the
confirm dialog's wording, and the navigation landing on the right
folder with the attribute consumed.
Closes#28
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #28 — the last item in #73's Phase 1.
Commits
21b303bfix(ui): stop a closing dialog answering the next questionwa-hidecancelled the nextconfirmAction()fe67849feat(autotag): name the confidence tier two features have to shareConfidentTier/Confident()9118c16feat(autotag): answer whether an album has a confident matchMatchForAlbum(albumID)b5d70acfeat(explore): offer the autotag match on the album pageICON_AUTOTAGThe three Direction points
1. A backend query cheap enough to call on page open. It costs no
MusicBrainz request at all. Everything
MatchForAlbumneeds is alreadyon disk:
tagging_itemscarries the top score and release from thebackground prefetch, and
tagging_candidatesdurably holds the scoredlist. A folder nobody has scored yet answers "nothing" rather than
scoring it now — the limiters here are shared with every page the user
can open, and a page that quietly spends a minute of somebody's
MusicBrainz budget to draw a banner is worse than a page that says
nothing.
2. The banner, beside
catalog-scope-noticewhere the issuesuggested, with "Apply tags" and "Review in Autotag".
3. The threshold defined once.
autotag.ConfidentTier— because#90 asks for exactly this: the album page and strict auto-accept must
agree on what "high confidence" means rather than computing it twice. A
page that claims confidence the auto-accept pass would decline is the
app contradicting itself. What they do not share is written down
beside it: surfacing is a suggestion behind a confirm dialog,
auto-accept is an irreversible rewrite gated on further conditions the
tier cannot express, so this is the floor both stand on rather than the
whole of either test.
Four decisions worth the review
The tier is computed, not read.
tagging_items.scoreis the rawnumber;
Recommendis what turns it into a claim, capping it for anambiguous runner-up, an incomplete alignment, or a folder too small to
corroborate itself. Filtering on the stored score would promise
confidence the scorer had explicitly withheld — pinned by the two-track
test, which scores 0.99 and is correctly not surfaced.
Applying is offered only where it would do the whole album. A
tagging group is a folder, so a multi-disc album is several. One button
applying to the best-scoring group would leave the album holding a mix
of old and new tags, which is the case the app's Blocking notification
level exists for.
groupCountis the test; the answer there is review.Nothing is said about an album the user already answered for.
confirmedcovers both a finished apply and an explicit "leave as is",and arguing with the second would be actively wrong.
The join is
audio_files.group_key, not a path-derived key. Agroup carved out of a mixed-bag folder is keyed on its tags, so a
path-derived key finds nothing for exactly the messiest libraries this
is meant to help.
A bug it found
confirm-dialogis one singleton for every confirmation in the app,and
wa-dialogreports its close asynchronously —open = falsestarts an animation and
wa-hidearrives after it. So a hide belongingto a question already answered lands after the next question has
opened and cancels it: the user is asked something, the dialog vanishes
on its own, and the call site is told they said no.
Found by writing two
confirmAction()tests in one file — the secondcould not be accepted at all. Reaching it in the app needs two
confirmations close together, which "Apply tags" makes possible. Each
ask carries an id now; only the
wa-hidehandler needs one, becauseonly
wa-hidecan arrive late.One vocabulary addition
ICON_AUTOTAG, onICON_PLAYLIST's rule — an icon names the noun itacts on, so a suggestion pointing at Autotag wears the Autotag
destination's own mark. It was written inline in the sidebar; two call
sites is where a name stops being one component's detail, so
icon-language.test.tsgoverns it now.Verification actually run
16 new specs across three files. Names go through
page.getByRole(...).And it was driven in the running app, with a confident match staged
into a real database through
/__test/sql:MatchForAlbumansweredstrongend to end, the banner rendered, the confirm dialog's impactline read correctly, and "Review in Autotag" landed on that group key
with the request attribute consumed (
hasAttribute('group-key') === false) — which is the half no component test can see, since Autotag isa cached primary view and the router is
index.ts.Note for #90
This leaves
ConfidentTierin place as the shared floor, andMatchForAlbumas a worked example of reading the durable candidatecache without touching the network — which is the same constraint #90's
auto-accept rule states ("uses already-cached candidate data — no
additional MusicBrainz calls").
Releases are manual (#115), so merging cuts no version.
Closes #28