import: the whole feature, and releases put back together

Everything the import grew over the last week, landing in one go — the
window, the picker, the audition pen, apply, and the review that stands
between them.

The fold is the new part. A release filed as a container of parts arrived
as N albums, because the scan groups audio by its immediate parent and the
container holds none of its own: Analord's eleven volumes were eleven
records, and six more releases split the same way. Records now fold into
the release above them, and apply is handed an explicit part list — never a
recursive walk, which is what let one stray file swallow 288 descendants
and duplicate 9 GB.

Where releases live is read off the collection rather than assumed, so a
genre-nested library moves the answer instead of breaking; below a dominant
majority at one depth the pass refuses entirely, because no folding beats
wrong folding. Three vetoes independent of depth catch a misread level.
Every fold is listed in the review with a way out.

research/import/fold-audit.mjs imports the shipping module, so the audited
code is the running code: 9 folds, 0 swallows across four library shapes,
and none of the 148 artist folders touched. It was that harness that killed
the tempting version of this rule — matching a part against its container
folded 40 artist folders, Mac Demarco's 8 albums into one, because album
folders carry the artist's name.

The regression test found a live bug on the way: parts number from 01 each,
so flattening two discs collided, and "same name, same size" read the
second file as already-imported and dropped it while still counting it.
That reuse now also requires that this run has not already written the
path.

Also here: the picker's accent crossfades between records instead of
cutting through the app's blue (ownership moved to the window, which
survives the per-record re-key), the fullscreen viewer wears the album's
accent instead of the one it left behind in the portal, Skip defers a
record without deciding it, and Keep mine is gone — with the cards picked
field by field it was Next under another name.
This commit is contained in:
KeelrLeek committed 2026-08-10 07:55:59 -05:00
1 parent 4b30c5e573
commit e7d0971de2
23 files changed
+6498 -258

No files matched your search

+63
View File
@@ -0,0 +1,63 @@
// The import review's match evidence — ONE definition, read by both the
// picker's verdict panel and the window's outcome buckets, so the filter
// and the panel can never disagree about the same record (the mock's rule,
// research/import/render.js evidence()).
export const normx = (s) => (s ?? "").toLowerCase().replace(/[^a-z0-9]/g, "");
const EDITION_WORD =
/\b(deluxe|expanded|remaster(?:ed)?|anniversary|edition|version|reissue|bonus|explicit|clean|mono|stereo|original|special|limited)\b/gi;
/** What a release is CALLED beyond the record's own name: bracketed years a
* ripper added, edition words a service adds. Strip both and two namings of
* one record meet. */
export const coreTitle = (s) =>
normx(
String(s ?? "")
.replace(/[\[(<][^\])>]*[\])>]/g, " ")
.replace(EDITION_WORD, " ")
.replace(/\b(19|20)\d{2}\b/g, " "),
) || normx(s);
/** What is actually known about whether candidate `c` is the same record as
* the scanned album (title + artist + your track count). Each row is a
* fact the user can check, not a score they have to trust. */
export function evidence(c, artist, title, trackCount) {
if (!c) return { items: [], verdict: "none", edition: false };
const artistOk = normx(c.artist) === normx(artist);
const titleOk = normx(c.title) === normx(title);
const titleClose =
!titleOk &&
(normx(c.title).includes(normx(title)) || normx(title).includes(normx(c.title)));
// Same record, named for a different edition — the ONE case where a
// different track count is expected rather than evidence of the wrong
// album.
const edition = !titleOk && coreTitle(title) === coreTitle(c.title);
const countOk = c.trackCount === trackCount;
const items = [
{ text: artistOk ? "Artist matches" : `Artist: ${c.artist}`, cls: artistOk ? "ok" : "no" },
{
text: titleOk
? "Title matches"
: edition
? `Same record, different edition: ${c.title}`
: titleClose
? `Title: ${c.title}`
: `Different title: ${c.title}`,
cls: titleOk ? "ok" : edition || titleClose ? "warn" : "no",
},
{
text: countOk
? `${c.trackCount} tracks, same`
: edition
? `${trackCount} tracks vs ${c.trackCount} — expected on another edition`
: `${trackCount} tracks vs ${c.trackCount}`,
cls: countOk ? "ok" : edition ? "warn" : "no",
},
];
const bad = items.filter((i) => i.cls === "no").length;
let verdict = bad >= 2 ? "bad" : bad === 1 ? "warn" : "ok";
// An edition difference is never "everything agrees".
if (edition && verdict === "ok") verdict = "warn";
return { items, verdict, edition };
}