chore: import issues, ignore build output
diff --git a/.gitignore b/.gitignore
@@ -4,3 +4,7 @@
*.log
.scratch/
__pycache__/
+*.bkp
+*~
+.DS_Store
+/intake
diff --git a/issues/0001-feat-opus-as-library-codec.md b/issues/0001-feat-opus-as-library-codec.md
@@ -0,0 +1,8 @@
+---
+title: feat: opus as library codec
+status: open
+labels: []
+created: 2026-09-13
+closed:
+---
+the library is vorbis right now because ffmpeg 9 drops tags on opus and can't attach cover art to ogg at all. so opus would need the opustags binary and still couldn't carry a picture. the opus path is still in there but only works with opustags installed. worth finishing properly, eventually
diff --git a/issues/0002-feat-sixel-iterm2.md b/issues/0002-feat-sixel-iterm2.md
@@ -0,0 +1,8 @@
+---
+title: feat: sixel/iterm2
+status: open
+labels: []
+created: 2026-09-13
+closed:
+---
+image previews only work on kitty (and ghostty/wezterm), with chafa as the fallback. terminals that do sixel or iterm2 images but not kitty just get the character preview. detectImageMode is where this would go. rasterm already encodes both so it's mostly the cell binding that needs writing here
diff --git a/issues/0003-bug-cover-art-leaks-between-albums.md b/issues/0003-bug-cover-art-leaks-between-albums.md
@@ -0,0 +1,14 @@
+---
+title: bug: cover art leaks between albums
+status: closed
+labels: []
+created: 2026-09-13
+closed:
+---
+processing imaginal disk straight after inferno, the cover art step came up saying it had already selected inferno's folder cover (Inferno.jpg, 2000x2000). that's the previous album's choice, not this one's.
+
+looks like planMsg resets screen/step/scroll but nothing else, so cover, hasCover, covers, coversFetched and coverCursor all carry over from the last album.
+
+coversFetched is the nasty one. it stays true, so the next album skips fetching its own candidates entirely and the picker offers you the previous album's. you could file an album with completely the wrong cover and nothing would warn you.
+
+saw it on imaginal disk after inferno.
diff --git a/issues/0004-feat-duplicate-cover-candidates.md b/issues/0004-feat-duplicate-cover-candidates.md
@@ -0,0 +1,8 @@
+---
+title: feat: duplicate cover candidates
+status: closed
+labels: []
+created: 2026-09-13
+closed:
+---
+the picker regularly shows the same image several times at different sizes. ive tested albums where it offered coverartarchive at 1200x1200 and again at 600x600, plus deezer and itunes versions of the same art. deduping by content hash would shorten the list a lot and make the choice less noisy
diff --git a/issues/0005-feat-way-to-file-a-batch-unattended.md b/issues/0005-feat-way-to-file-a-batch-unattended.md
@@ -0,0 +1,8 @@
+---
+title: feat: way to file a batch unattended
+status: closed
+labels: []
+created: 2026-09-13
+closed:
+---
+--yes exists but refuses when a plan has hard issues, which is right, but there's no way to say "file everything you can and list what you couldn't". working through a big staging queue means confirming every album one at a time (which in some regards is great, but some might consider this more annoying than nice). a resume-style loop that files what it can and stops on anything hard would be good to have
diff --git a/issues/0006-tweak-a-failed-journal-write-is-only-a-warning.md b/issues/0006-tweak-a-failed-journal-write-is-only-a-warning.md
@@ -0,0 +1,8 @@
+---
+title: tweak: a failed journal write is only a warning
+status: closed
+labels: []
+created: 2026-09-13
+closed:
+---
+if the journal can't be written and the album still gets filed, a warning just gets printed. that leaves an album in the archive the journal doesn't know about so library verify and library audit won't see it and a later run won't know it's already done. for something that only runs once per album i'd rather it stop than carry on.
diff --git a/issues/0007-tweak-re-encoding-a-cover-happens-on-the-ui-gorout.md b/issues/0007-tweak-re-encoding-a-cover-happens-on-the-ui-gorout.md
@@ -0,0 +1,8 @@
+---
+title: tweak: re-encoding a cover happens on the UI goroutine
+status: closed
+labels: []
+created: 2026-09-13
+closed:
+---
+a 2000x2000 jpeg takes about 270ms to decode, scale and re-encode so moving the cursor between candidates feels kinda shit. measured it while looking at something else. errr, should either move it to a background command or cache more aggressively (?)
diff --git a/issues/0008-bug-cover-preview-leaks-a-temp-directory-per-image.md b/issues/0008-bug-cover-preview-leaks-a-temp-directory-per-image.md
@@ -0,0 +1,8 @@
+---
+title: bug: cover preview leaks a temp directory per image
+status: closed
+labels: []
+created: 2026-09-13
+closed:
+---
+stageTempImage does an os.MkdirTemp for each cover it stages and only removes the directory on the error path. nothing cleans up afterewards and kitty doesn't delete the file for the t=f medium, so every cover you look at in the picker leaves a temp file until you reboot (to clear out /tmp). annoyingggg. should reuse one directory or clean up when the picker closes.
diff --git a/issues/0009-bug-disc-prefix-config-option-does-nothing.md b/issues/0009-bug-disc-prefix-config-option-does-nothing.md
@@ -0,0 +1,8 @@
+---
+title: bug: disc_prefix config option does nothing
+status: closed
+labels: []
+created: 2026-09-13
+closed:
+---
+the disk_prefix option is parsed and defaults to "always", but nothing actually reads it. TrafckFile in normalize always writes the disc number, so setting disk_prefix = "never" silently has no effect. will either hook this up or drop it. also is worth deciding what it should do for single disc albums too, since irght now every album gets a 1- prefix even when there's only one disk.
diff --git a/issues/0010-feat-lossy-sources-in-the-archive.md b/issues/0010-feat-lossy-sources-in-the-archive.md
@@ -0,0 +1,12 @@
+---
+title: feat: lossy sources in the archive
+status: open
+labels: []
+created: 2026-09-14
+closed:
+---
+the archive is flac only. an album containing an mp3 is now refused up front with a message saying so, which is better than the old behaviour (accepted by the plan, then failed deep inside metaflac after the files had been copied), but it still means an mp3 release cannot be filed at all. that is a large slice of what soulseek actually serves.
+
+doing it properly needs two decisions before any code. first, does a lossy file belong in the archive as a master, when it is the best copy that exists? the archive is described as lossless masters, so this changes what it means. second, should the library copy be transcoded from it? mp3 to vorbis is a second generation loss, so copying the original through might be better than transcoding.
+
+the rest is plumbing: ffmpeg can write id3 tags and attach cover art, and the verification already counts artwork for any format.
diff --git a/issues/0011-feat-library-audit-fix.md b/issues/0011-feat-library-audit-fix.md
@@ -0,0 +1,14 @@
+---
+title: feat: library audit --fix
+status: closed
+labels: []
+created: 2026-09-14
+closed:
+---
+An audit that only reports leaves the same chore every time.
+
+--fix rebuilds the library copies that are missing or short, and restores a missing cover.jpg by copying the archive's rather than re-encoding the album, which would be wasted work for one file.
+
+It never writes to the archive. The masters are the one thing a rebuild cannot replace, so a missing or empty archive directory is reported and left strictly alone, and so is an album in the archive that no record names.
+
+Without --fix the report now says that --fix exists, and both modes count what was repaired in --json.
diff --git a/issues/0012-feat-keep-an-album-s-edits-between-runs.md b/issues/0012-feat-keep-an-album-s-edits-between-runs.md
@@ -0,0 +1,12 @@
+---
+title: feat: keep an album's edits between runs
+status: closed
+labels: []
+created: 2026-09-14
+closed:
+---
+An edited album used to live only in memory, so quitting the interface, or opening a different album and coming back, threw away every correction that had been typed. The sheet is already a complete description of what intake is about to write, so keeping it on disk is all that is needed to make a review resumable.
+
+Edits are saved under <state>/review/, named after the staging folder plus a short digest of the whole path so a directory listing stays readable while two albums with the same folder name in different places cannot collide. Opening an album applies the saved sheet and says so; the interface offers D to discard it; and filing the album drops it, because leaving it would apply last time's corrections to a fresh folder of the same name.
+
+The same store makes the command line repeatable: intake album <name> twice in a row does the same thing the second time rather than starting from the tags again. A saved sheet that no longer fits the album -- the files changed underneath it -- is discarded with a note rather than left to fail on every run.
diff --git a/issues/0013-feat-a-help-overlay.md b/issues/0013-feat-a-help-overlay.md
@@ -0,0 +1,14 @@
+---
+title: feat: a ? help overlay
+status: closed
+labels: []
+created: 2026-09-14
+closed:
+---
+The keys were only in the footer, and only for the screen you were on, in a terse form.
+
+? now opens an overlay listing every key that does something on the current screen with a one-line description, plus the keys that work everywhere. It is an overlay rather than a screen, so the screen underneath keeps its scroll and cursor and closing it returns exactly where the user was; ? or esc closes it; ctrl+c still quits; and while it is open it takes every other key, so nothing fires by accident while somebody is reading it.
+
+The list is derived from one function per screen that returns key/description pairs, and the footer now renders its hints from the same list, so the two cannot drift apart. Conditional states are included: the browse filter, the journal action menu (where enter means confirm), and the review screen when an album that is already in the library has taken k and r for keep-both and replace, leaving the arrow keys as the only way to scroll up.
+
+It fits the terminal and degrades sanely on a small one: it drops screen keys before global ones, and on a tiny terminal shows a single line saying how to close it.
diff --git a/issues/0014-feat-rename-or-merge-an-artist-folder.md b/issues/0014-feat-rename-or-merge-an-artist-folder.md
@@ -0,0 +1,13 @@
+---
+title: feat: rename or merge an artist folder
+status: closed
+labels: []
+created: 2026-09-14
+closed:
+---
+Two spellings of one artist is the most common mess a rebuild produces, and fixing it by hand means moving two directories, editing every journal record underneath them and finding every artists.toml entry that named the old name. Missing any one of those leaves the journal describing directories that no longer exist, which library verify then reports as albums that have gone missing.
+
+intake artist rename <old> <new> moves the artist folder in the archive and the library and repoints every journal record and alias beneath it.
+intake artist merge <old> <into> moves every album from one artist folder into another. A clash -- an album of that name already in the destination -- stops the whole thing before anything moves, rather than leaving one album on top of another.
+
+The audio moves; nothing is copied or deleted. A rename refuses to run when the destination already exists, and tells the user to merge instead.
diff --git a/issues/0015-feat-find-cover-art-in-subfolders-and-re-cover-eve.md b/issues/0015-feat-find-cover-art-in-subfolders-and-re-cover-eve.md
@@ -0,0 +1,14 @@
+---
+title: feat: find cover art in subfolders, and re-cover every placeholder at once
+status: closed
+labels: []
+created: 2026-09-14
+closed:
+---
+Two halves of the same problem: art that was sitting right there and never got used, and albums stuck on a generated placeholder with no way to retry them in bulk.
+
+Art is now found one level below the album as well as beside the tracks. That is how scans arrive -- a Scans/ or Artwork/ folder holding front.jpg -- and it used to be invisible, so an album whose real sleeve was in the staging folder was filed with a placeholder and the art stayed in staging for ever. Art beside the tracks still wins outright; the subfolder is only a fallback, so a stray back cover in extras/ cannot displace the real front cover. A candidate found in a subfolder says where it came from, so the list reads "Scans/front.jpg".
+
+intake library re-cover --placeholders runs the fetch again for every album still on a placeholder, sorted by artist and album, reporting each one. Only a confident match is taken: replacing a placeholder with the wrong sleeve is worse than keeping the placeholder, so anything less is left alone and counted separately.
+
+Both paths go through one function that writes the cover, embeds it in the masters, records the change and rebuilds the library copy, because the library has its own cover.jpg and its own embedded picture. Replacing the archive art without that left the placeholder list clear while the copy people actually stream still carried the placeholder.
diff --git a/issues/0016-feat-ignore-a-staging-folder-for-good.md b/issues/0016-feat-ignore-a-staging-folder-for-good.md
@@ -0,0 +1,14 @@
+---
+title: feat: ignore a staging folder for good
+status: closed
+labels: []
+created: 2026-09-14
+closed:
+---
+Junk folders and releases somebody is not ready to deal with keep coming back in every album list. intake ignore adds a folder name to <root>/ignore.txt, beside staging, archive and library, and scan, the album list and a batch all skip it.
+
+Names rather than paths, because a folder called "Various Artists - Singles" that turned out to be a mess should stay ignored whatever happens to staging in between.
+
+The file is a plain list with comments and is meant to be hand-editable; adding and removing rewrite it sorted so it stays readable.
+
+intake ignore with no arguments lists what is ignored, --remove takes names back off the list, and --json gives the list to a script. Ignoring never touches the folder: it stays in staging exactly as it was.
diff --git a/issues/0017-feat-build-journal-records-for-an-archive-that-has.md b/issues/0017-feat-build-journal-records-for-an-archive-that-has.md
@@ -0,0 +1,17 @@
+---
+title: feat: build journal records for an archive that has none
+status: closed
+labels: []
+created: 2026-09-14
+closed:
+---
+The journal is the only index intake has, and an archive can exist without it: albums filed by an older version, albums copied in by hand, or an album whose record was lost. Those albums are invisible to verify and audit, and a batch would file a second copy beside them.
+
+intake library adopt walks the archive, finds the album directories no record names, reads the artist and album from the masters' own tags -- the folder names may have been normalised on the way in and no longer say what the album was called -- and writes a record for each. Nothing is written to the audio.
+
+Two details worth knowing:
+
+* The record is keyed by the archive directory rather than by a staging path, because the staging path is long gone and inventing one would be a lie. Record.Adopted marks these so they are distinguishable from albums this tool filed.
+* An adopted album with no cover.jpg is flagged as needing art, so it turns up in the placeholder list instead of looking finished.
+
+Adopting refuses nothing and reports what it will do before doing it; --yes skips the confirmation and --dry-run changes nothing. Running it again finds nothing, which is what makes it safe to run after any import.
diff --git a/issues/0018-feat-notice-an-album-i-already-have-before-reviewi.md b/issues/0018-feat-notice-an-album-i-already-have-before-reviewi.md
@@ -0,0 +1,18 @@
+---
+title: feat: notice an album I already have before reviewing it
+status: closed
+labels: []
+created: 2026-09-14
+closed:
+---
+The destination path is not enough to answer "do I already have this": a re-download whose artist folder is spelled differently lands somewhere else entirely, and the user only finds out after reviewing the cover of an album they already own.
+
+An album is now matched against the journal on the folded artist and album names -- case, spacing and punctuation removed -- as well as against the disk on the destination path. Matching on tags is what catches the re-download; matching on the path is what catches an album copied in by hand with no record at all.
+
+The finding is shown on the metadata step, before the review starts, with the three choices one keypress each:
+
+* keep both (the default, because it is the only choice that cannot lose an album) files the new copy beside the old one as "<album> (2)" and follows it there for the summary, the journal and the library built by the same run;
+* replace removes the album already there once the new one has landed;
+* skip leaves this album alone and goes back to the list.
+
+The keys take priority over scrolling on that step, because k would otherwise scroll the screen, which is exactly the kind of small collision that makes somebody press the wrong thing at the wrong moment.
diff --git a/issues/0019-feat-file-a-batch-unattended-parking-what-needs-a.md b/issues/0019-feat-file-a-batch-unattended-parking-what-needs-a.md
@@ -0,0 +1,16 @@
+---
+title: feat: file a batch unattended, parking what needs a decision
+status: closed
+labels: []
+created: 2026-09-14
+closed:
+---
+intake batch files every ready album in one pass. An album that needs a person is parked -- recorded with the reason and left exactly where it is -- and the run moves on. A genuine failure is reported and also not fatal, because the next album has nothing to do with it; the command exits non-zero at the end if anything failed, so a script can tell "finished" from "finished with problems".
+
+This is the biggest practical blocker on a large collection: one mistagged mp3 in a directory of two hundred downloads used to stop everything behind it, and resume stopped at the first ready album whatever state it was in.
+
+Parking is a journal phase (needs_attention), not a line in a log, so the album stays findable: library verify and the journal screen show it with its reason, and running the batch again parks it again rather than losing it.
+
+In the interface, b on the album list runs the same batch with the progress on the working screen and a summary that names every album it parked, rather than only counting them. The album list marks parked albums in place of their status line, and n jumps the cursor to the next one, which is how somebody works through the backlog a batch leaves behind.
+
+Closes 1c36deb
diff --git a/issues/0020-feat-a-journal-screen-with-forget-and-un-file.md b/issues/0020-feat-a-journal-screen-with-forget-and-un-file.md
@@ -0,0 +1,21 @@
+---
+title: feat: a journal screen, with forget and un-file
+status: closed
+labels: []
+created: 2026-09-14
+closed:
+---
+J on the album list opens the journal: every record with what is actually on disk beside it, and per record a menu of what can be done to it.
+
+Each line shows the artist and album, the phase, and any disagreement with the disk -- archive missing, placeholder cover, no library copy, or an album in the archive that no record names at all. That last kind is the one nothing else surfaces.
+
+Two actions, chosen from a menu rather than remembered as keys:
+
+* Forget it. The record goes and every file stays exactly where it is. This is a statement about what intake remembers, not about what is on disk.
+* Un-file it. The masters move back to staging so the album can be reviewed again, and the record goes with them. The library copy is removed, because it is a transcode of those masters and would otherwise sit there describing an album that is no longer archived. The move is a rename, not a copy and a delete: after filing, the archive holds the only copy of that audio, because the staged originals were removed once the archive verified.
+
+Un-filing is the only place in the interface that moves an album already in the archive, so it takes two keypresses: the first says what will happen, the second does it.
+
+A record with nothing behind it -- an album in the archive that no journal record names -- offers no actions and says so, because forgetting it and moving it are both impossible.
+
+The same two operations are available without the interface, as intake library forget <album> and intake library unfile <album>, and the journal listing is exposed to the interface through the runner rather than the TUI reaching into the journal itself.
diff --git a/issues/0021-feat-one-album-sheet-editable-from-every-screen-be.md b/issues/0021-feat-one-album-sheet-editable-from-every-screen-be.md
@@ -0,0 +1,21 @@
+---
+title: feat: one album sheet, editable from every screen before filing
+status: closed
+labels: []
+created: 2026-09-14
+closed:
+---
+Everything intake is about to write is editable in a single $EDITOR document, reachable with e from every screen that has a plan loaded except the one that is filing it. That includes the cover picker: choosing a cover is exactly when a wrong album artist gets noticed, and having to back out of the picker first would make the editor useless at the moment it is most wanted.
+
+The sheet holds the album, album artist, both folder names, the year, the cover, and per track the title, artist, disc, track number and an optional file name override.
+
+It replaces the track-name editor, which could change only the two folder names and the text after each track number; the album title, album artist, year, disc and track numbers and a single track's artist were all decided from the tags and could only be put right by retagging the files.
+
+Two behaviours are new rather than carried over:
+
+* Deleting a track block now really does leave that file out of the album. The old editor told people a deleted line skipped the file and then filed it anyway, under the name intake had already chosen for it. The per-disc track totals and the disc total are counted again after a drop or a renumber, so a stale TRACKTOTAL cannot make a player show a finished album as incomplete.
+* A folder that still holds the name intake derived it from follows a rename of the album or artist. One that was typed on purpose does not.
+
+Track identity is the number after ##, which is the track position when the sheet was written. It is deliberately not the file name (editable), the track number (editable) or the row order (sortable). A value may contain # and : freely, because a comment is only ever a whole line; the old editor stripped an inline comment and quietly truncated any title containing " #".
+
+An empty sheet leaves the album alone, an album of mp3s is not sent to the editor at all (the sheet cannot make a lossy file lossless), and --edit-names forces it open.
diff --git a/issues/0022-fix-verify-decode-accepts-a-value-that-silently-sk.md b/issues/0022-fix-verify-decode-accepts-a-value-that-silently-sk.md
@@ -0,0 +1,12 @@
+---
+title: fix: verify_decode accepts a value that silently skips the decode check
+status: closed
+labels: []
+created: 2026-09-14
+closed:
+---
+Found by the audit of 2026-09-14.
+
+The decode check after filing runs on the sampled files, on every file, or not at all. An unrecognised verify_decode was stored verbatim and then matched none of those modes, so a typo quietly turned the only end-to-end check of the audio that gets written into a no-op. It is now restricted to all, sample or none.
+
+quality is range-checked at the same time: libvorbis accepts 1-10, and anything outside that reached ffmpeg and failed the whole album with an encoder error instead of a configuration one.
diff --git a/issues/0023-fix-an-album-can-sit-in-the-archive-with-no-journa.md b/issues/0023-fix-an-album-can-sit-in-the-archive-with-no-journa.md
@@ -0,0 +1,14 @@
+---
+title: fix: an album can sit in the archive with no journal record
+status: closed
+labels: []
+created: 2026-09-14
+closed:
+---
+Found by the audit of 2026-09-14.
+
+The journal is the only thing that knows an album exists. An album is promoted into the archive before its record is written, so a process killed in between -- OOM, power loss, SIGKILL -- leaves a complete album that verify says nothing about, audit does not count, and a later run happily files a second copy beside.
+
+Neither command looked at the archive at all, so it could not notice. library verify now lists every album directory in the archive that no journal record names, and library audit counts them as needing attention. Both report the count in --json.
+
+The same commit fixes a name-search loop in the "both" collision path: it treated any error from os.Stat as "the name is free" unless it was NotExist, so an unreadable parent directory would have spun looking for a name it could never check.
diff --git a/issues/0024-fix-dead-code-and-staticcheck-findings.md b/issues/0024-fix-dead-code-and-staticcheck-findings.md
@@ -0,0 +1,10 @@
+---
+title: fix: dead code and staticcheck findings
+status: closed
+labels: []
+created: 2026-09-14
+closed:
+---
+Found by the audit of 2026-09-14 with staticcheck -checks=all.
+
+Removed unused functions (sortedDirs, artistDirOfPlan, alArtistCredit, editorFixesAll, Plan.nameSession, a dead test helper), the ineffective assignment to the never-read Module.contentRows field, a duplicated condition in scan.collectAudio, and two loops staticcheck flagged in internal/audio. staticcheck is now clean.
diff --git a/issues/0025-fix-journal-updates-can-lose-records-and-the-repor.md b/issues/0025-fix-journal-updates-can-lose-records-and-the-repor.md
@@ -0,0 +1,30 @@
+---
+title: fix: journal updates can lose records and the reports can be wrong
+status: closed
+labels: []
+created: 2026-09-14
+closed:
+---
+Found by the audit of 2026-09-14.
+
+* Every journal update was an unlocked open-modify-save, and every save used a fixed "<journal>.tmp" name. Two intake runs on one root each read the journal, each filed an album, and the second save wrote back a snapshot taken before the first, so the first album's record vanished while the album stayed on disk -- invisible to verify and audit, and free to be filed again. Saves now use a unique temporary file, and each read-modify-write runs under a lock file beside the journal, with a stale lock broken rather than waited on for ever.
+
+* forget() was called on any execute failure, including one that happened before a single byte was copied, and it deleted the record of an album that was still in the archive. It now only clears a record whose album has gone.
+
+* record() checked whether a library had already been built after writing the new record, so it always saw false and downgraded the flag. verify then reported an album whose library copy is on disk as one without a library copy.
+
+* library transcode rebuilt the copy without telling the journal, so verify went on warning however many times it was rebuilt.
+
+* library audit skipped a record whose archive was missing or held no audio, and then reported "0 needing attention" -- the one thing it exists to notice. verify accepted an existing but empty archive directory as present.
+
+* library audit hard-coded ".ogg", so every album in an opus library was reported incomplete for ever.
+
+* --json printed the human-readable problems before the JSON, so the output was unparseable exactly when it had something to say. library verify --json also omitted the missing count.
+
+* findRecord folded a punctuation-only target to the empty string, and a substring search for "" matches every album, so "intake library transcode !!!" picked whichever album happened to be alone in the journal.
+
+* Resume stopped at the first ready album whatever state it was in, so one mistagged mp3 meant no album behind it was ever reached; and the refusal was reported as success, so the command exited zero.
+
+* library re-cover replaced the archive art but never rebuilt the library, leaving the placeholder list clear while the copy people stream still carried the placeholder.
+
+* --dry-run in the TUI reached execute.Run without the flag being passed on, so previewing an album wrote it to the archive and cleared staging.
diff --git a/issues/0026-fix-the-library-copy-can-lose-its-cover-or-be-dest.md b/issues/0026-fix-the-library-copy-can-lose-its-cover-or-be-dest.md
@@ -0,0 +1,12 @@
+---
+title: fix: the library copy can lose its cover or be destroyed by a failed rebuild
+status: closed
+labels: []
+created: 2026-09-14
+closed:
+---
+Found by the audit of 2026-09-14.
+
+The commonest shape a Soulseek album arrives in is a folder cover.jpg and no embedded art. The cover is chosen from the staging folder, execute.Run copies and embeds it into the archive and then deletes it as a staging leftover, and the library is built after that from the same now-deleted path. library.Transcode skipped a cover it could not stat without a word, so the library was built with no artwork at all while the archive had the right cover and every summary still named it. A copy of the cover is now taken before the archive runs, and a cover that cannot be read is reported as a warning.
+
+The library swap was RemoveAll(old) followed by Rename(new). A failure part-way through the RemoveAll left the old library half deleted, the new one was thrown away by the deferred cleanup because the rename never happened, and there was a window between the two calls in which neither copy existed. The old library is now moved aside first and moved back if the new one cannot land. A failed cover optimisation also used to leave whatever ImageMagick had half-written to be promoted into the library, where library audit only ever asked whether a cover.jpg existed.
diff --git a/issues/0027-fix-the-archive-write-path-loses-tracks-and-destro.md b/issues/0027-fix-the-archive-write-path-loses-tracks-and-destro.md
@@ -0,0 +1,24 @@
+---
+title: fix: the archive write path loses tracks and destroys albums when it fails
+status: closed
+labels: []
+created: 2026-09-14
+closed:
+---
+Found by the audit of 2026-09-14. Every item below was reproduced with a Go test against the real code.
+
+* Two tracks in one album that share a track number and a title are both given the same target name, and the copy truncates, so the second silently replaces the first. The album is filed a track short and verification passes, because it compares each written file against the plan entry in the same position. normalize.DetectCollisions groups by the title half of a name and folds repeats of a title together, so it never saw this pair. There is now a plan-level issue and a hard guard in execute.Run that refuses before the first copy.
+
+* Collision "both" renames the album to "<album> (2)" but every later step kept using the path the plan asked for: the journal recorded the pre-existing album, the library was transcoded from the pre-existing masters, and the bytes reported were the pre-existing album's. The promoted directory is now returned and followed.
+
+* "replace" ran RemoveAll on the destination and only then renamed the new album in. A failed rename, a permission problem part-way through RemoveAll, or a power cut left the album gone with nothing to restore. The old album is now moved aside first and moved back if the new one cannot land.
+
+* "merge" checked for conflicts as it moved files, so a clash on the second file left the destination holding part of one album and part of another. Every name is now checked before anything moves.
+
+* A staged file that could not be deleted after a successful promotion made Run return an error; FilePlan then called forget() and deleted the journal record of an album that is complete and on disk. Staging cleanup failures are now reported without failing the run.
+
+* cleanStaging walked up to four empty ancestor directories, which reached past the album and past staging: filing the last album could delete the staging directory itself, making the next scan fail. It now stops at the staging root, and clears the album folder the scan named rather than the disc folder the first track happened to sit in, so a CD1/CD2 rip no longer leaves CD2 and the album sidecars behind for ever.
+
+* A failed run left the artist folder it had created, empty, in the archive.
+
+* Run panicked on a plan with no tracks.
diff --git a/issues/0028-fix-an-unattended-run-can-file-an-album-under-a-si.md b/issues/0028-fix-an-unattended-run-can-file-an-album-under-a-si.md
@@ -0,0 +1,20 @@
+---
+title: fix: an unattended run can file an album under a similar artist's folder
+status: closed
+labels: []
+created: 2026-09-14
+closed:
+---
+Found by the audit of 2026-09-14. Reproduced end to end with scripts/make-edge-fixtures.sh: four albums by four different artists were filed into four other artists' folders.
+
+While resolving an artist, a fuzzy candidate at or above 0.75 similarity was taken automatically under --yes, and written to artists.toml, so every later album by that credit followed it there. "Blur" and "Blue" score 0.750; "The Cure" and "The Cult" 0.750; "RTL Artist" and "Art Artist" 0.800. Nothing downstream notices, because no check compares a folder name against the ALBUMARTIST tag.
+
+Measured on the edge fixtures before the fix:
+ Cue Artist -> dupe-artist (fuzzy match 82%, auto-selected)
+ RTL Artist -> art-artist (fuzzy match 80%, auto-selected)
+ Long Artist -> loose-artist (fuzzy match 75%, auto-selected)
+ Slash Artist -> scan-artist (fuzzy match 75%, auto-selected)
+
+A similar name is now only ever offered, never taken. An unattended run creates the folder the credit names and prints the exact artists.toml line to add if the two really are the same artist. The interactive prompt still lists the candidates, but its default is the new folder rather than the guess.
+
+The same commit fixes the alias round trip: TOMLKey writes keys with %q and the reader only trimmed the outer quotes, so a credit containing a quote or a backslash -- "Weird Al" Yankovic, say -- never matched the entry written for it.