diff --git a/.planning/NOTES.md b/.planning/NOTES.md
index 86e4dcd..e2687de 100644
--- a/.planning/NOTES.md
+++ b/.planning/NOTES.md
@@ -3506,3 +3506,31 @@ under a name the reader does not look at is indistinguishable from one
never written. So the tests assert the round trip through
`metadata.ExtractTags` — the reader the *scan* uses — rather than
through the bytes the writer produced.
+
+## The published catalog artifact predates `total_tracks` (measured 2026-08-18)
+
+```
+$ curl -sSI .../generic/yellowjacket-core-index/latest/core-index.db.zst
+last-modified: Mon, 10 Aug 2026 04:38:16 GMT
+content-length: 75417037
+
+$ sqlite3 core-index.db \
+ "SELECT COUNT(*) FROM pragma_table_info('explore_index') WHERE name='total_tracks';"
+0
+$ sqlite3 core-index.db "SELECT COUNT(*) FROM explore_index;"
+1079667
+```
+
+The column landed in the schema on 2026-08-16; the artifact is from
+08-10, and `index-artifact.yml` is a weekly cron, not a push trigger.
+So `completenessAnswer()`'s catalog fallback answers 0 for **every**
+user today — the machinery is correct and `artifactHasTotals()` is
+doing precisely its job, there is just no data behind it. Same position
+the credit tables are in; both ride on the next publish (#88).
+
+The general point, which is why this is written down rather than just
+fixed: **a probe that makes a column optional also makes its absence
+silent.** `artifactHasTotals` and `artifactHasCredits` are both correct
+and both mean a feature can ship, pass every test, and produce nothing
+for anybody without a single failure anywhere. Checking the *published
+file* is one query and is not implied by any tick in CI.
diff --git a/CLAUDE.md b/CLAUDE.md
index 7d861bc..0e603b7 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1623,6 +1623,32 @@ side-effect worth knowing: this is what finally makes `ownership()`
say something true here, since counting the displayed tracklist of a
library-only entry could only ever produce "9 of 9".
+**And it can be asked, because the rule alone reaches too few albums.**
+That guard depends on two inputs the user does not control: the files
+declaring a per-disc total, and the catalog's own `total_tracks`. Where
+neither says — which is a great deal of any library, and *every* library
+until an artifact carrying the column is published — a partly-owned
+album showed only the tracks on disk with nothing to say the rest
+existed. `renderTracklistScope()` is the explicit route: a
+"Show the whole album" switch that flips the synthetic "Your Library"
+entry between the local files and the release, which is the rendering
+the page could already do and could only be *triggered* automatically.
+
+Three things about it are load-bearing. **`showFullTracklist` is a
+tri-state**, `null` meaning "follow the automatic rule": the rule is
+right when it fires and the switch has to be able to agree with the page
+it sits on rather than starting out contradicting it, which a plain
+boolean would need recomputed every time the completeness answer moved
+underneath it. **`fullReleaseCluster()` falls back to the
+highest-scoring cluster**, because `findLibraryCluster` is a guess over
+the `inLibrary` flags and returns *nothing* when none are set — which is
+exactly the untagged library the switch exists for, so without the
+fallback the control would be absent precisely where it is needed. And
+**it is shown only where it can change what is on screen**: against the
+library entry, with a release to switch to, and only when the two
+tracklists differ — the same test the version dropdown answers, one
+control over.
+
**A dropdown is only a choice if the choices differ.** The version
selector tested `versionEntries.length`, but a release group routinely
has several releases — reissues, regional pressings, a remaster — whose
diff --git a/frontend/src/components/explore-album-details/explore-album-details.ts b/frontend/src/components/explore-album-details/explore-album-details.ts
index f447e96..bb48aac 100644
--- a/frontend/src/components/explore-album-details/explore-album-details.ts
+++ b/frontend/src/components/explore-album-details/explore-album-details.ts
@@ -193,6 +193,25 @@ export class ExploreAlbumDetails extends LitElement implements ContextMenuHost {
@state() private selectedVersionKey: string = '';
@state() private coverArtURL = '';
+ /**
+ * Whether to draw the whole release rather than only the files on
+ * disk — `null` while nobody has said, which is the automatic rule
+ * (`buildLibraryEntry`: show the release once the tags say the album
+ * is incomplete).
+ *
+ * It is a *tri-state* on purpose. The automatic rule is right when
+ * it fires and the switch has to be able to agree with it, or the
+ * control would start out contradicting the page it is sitting on;
+ * a plain boolean would need its default recomputed every time the
+ * completeness answer changed underneath it.
+ *
+ * The rule alone was not enough, which is the report: it depends on
+ * the files declaring a per-disc total, so a library whose tags
+ * never said sat permanently on "only my tracks" with no way to ask
+ * for the rest — and no way to tell that there was a rest.
+ */
+ @state() private showFullTracklist: boolean | null = null;
+
/**
* The local album's own tracks — the authoritative answer to "what
* is actually on disk," independent of `this.releases`, which
@@ -559,6 +578,19 @@ export class ExploreAlbumDetails extends LitElement implements ContextMenuHost {
}
/* ── Tracklist ── */
+ .tracklist-scope {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 6px 12px;
+ margin-bottom: 8px;
+ }
+
+ .tracklist-scope-hint {
+ font-size: var(--yj-text-xs);
+ color: var(--yj-text-tertiary, #888);
+ }
+
.tracklist {
display: flex;
flex-direction: column;
@@ -914,6 +946,7 @@ export class ExploreAlbumDetails extends LitElement implements ContextMenuHost {
this.releases = [];
this.versionEntries = [];
this.selectedVersionKey = '';
+ this.showFullTracklist = null;
this.localTracks = [];
this.filePaths = new Map();
this.askedFor = new Set();
@@ -1817,17 +1850,23 @@ export class ExploreAlbumDetails extends LitElement implements ContextMenuHost {
// Guarded on `known` rather than on "fewer tracks than the
// cluster", which would swap in a catalog tracklist for
// every album whose tags simply never declared a total.
+ //
+ // And guarded on the *user's* answer first, because the
+ // automatic rule can only fire where the tags declared a
+ // total: an album that says nothing is not an album that is
+ // complete, and it used to be shown as one.
const answer = this.completenessAnswer();
- const incomplete = answer?.known && !answer.complete;
- if (incomplete) {
- const fullRelease = this.findLibraryCluster(clusters);
+ if (this.showFullTracklist ?? (answer?.known && !answer.complete)) {
+ const fullRelease = this.fullReleaseCluster(clusters);
if (fullRelease) {
return {
key: 'synthetic:library',
label: 'Your Library',
- sublabel: `${answer?.owned ?? 0} of ${answer?.expected ?? 0} tracks · ${this.clusterLabel(fullRelease)}`,
+ sublabel: answer?.known
+ ? `${answer.owned} of ${answer.expected} tracks · ${this.clusterLabel(fullRelease)}`
+ : `${this.clusterLabel(fullRelease)} · full tracklist`,
group: 'aggregate',
syntheticKind: 'library',
tracks: fullRelease.representative.tracks ?? [],
@@ -1861,6 +1900,25 @@ export class ExploreAlbumDetails extends LitElement implements ContextMenuHost {
};
}
+ /**
+ * The release to draw when the whole album is wanted rather than
+ * the files on disk.
+ *
+ * `findLibraryCluster` is the right answer where it has one — the
+ * release the user's tracks overlap most — but it is a guess over
+ * the `inLibrary` flags and returns nothing at all when none of
+ * them are set, which is every untagged library. Falling back to
+ * the highest-scoring cluster is what makes the switch work there;
+ * that is the same release the page would call "Standard", and the
+ * sublabel names it either way rather than leaving the user to
+ * wonder whose tracklist they are reading.
+ */
+ private fullReleaseCluster(
+ clusters: ReleaseCluster[],
+ ): ReleaseCluster | undefined {
+ return this.findLibraryCluster(clusters) ?? clusters[0];
+ }
+
/**
* Fallback only: used when there's no local album to anchor on
* (see `buildLibraryEntry`). Finds the cluster with the highest
@@ -2238,6 +2296,7 @@ export class ExploreAlbumDetails extends LitElement implements ContextMenuHost {
@catalog-retry=${this.retryCatalog}
>
${this.renderVersionSelector()}
+ ${this.renderTracklistScope()}
${this.renderTracklist()}