diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index b783e6e..5a2ed7b 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -68,7 +68,11 @@
3. Editing a WAV file's tags does not alter audio playback — the file sounds identical before and after
4. Existing metadata in the WAV file that wasn't edited (RIFF INFO chunks, bext, cue markers) survives the tag write unchanged
5. If the app crashes or loses power during a WAV tag write, the original file is intact (not corrupted or truncated)
-**Plans**: TBD
+**Plans:** 2 plans
+
+Plans:
+- [ ] 19-01-PLAN.md — WAV RIFF parser/writer and writeWavTags function
+- [ ] 19-02-PLAN.md — WAV tag writer round-trip tests
### Phase 20: OGG Vorbis Tag Writer
**Goal**: Users can edit metadata and cover art on OGG Vorbis files with the same experience as MP3/FLAC/WAV
@@ -113,7 +117,7 @@
| 16. Tag Writing & Database Sync | v1.2 | 3/3 | Complete | 2026-03-17 |
| 17. Single Track Edit | v1.2 | 2/2 | Complete | 2026-03-18 |
| 18. Batch Edit | v1.2 | 2/2 | Complete | 2026-03-18 |
-| 19. WAV Tag Writer | v1.2.1 | 0/? | Not started | - |
+| 19. WAV Tag Writer | v1.2.1 | 0/2 | Not started | - |
| 20. OGG Vorbis Tag Writer | v1.2.1 | 0/? | Not started | - |
| 21. Cleanup | v1.2.1 | 0/? | Not started | - |
diff --git a/.planning/phases/19-wav-tag-writer/19-01-PLAN.md b/.planning/phases/19-wav-tag-writer/19-01-PLAN.md
new file mode 100644
index 0000000..feed9b2
--- /dev/null
+++ b/.planning/phases/19-wav-tag-writer/19-01-PLAN.md
@@ -0,0 +1,283 @@
+---
+phase: 19-wav-tag-writer
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - backend/tagwriter/wav.go
+ - backend/tagwriter/tagwriter.go
+ - backend/tagwriter/mp3.go
+ - backend/tagwriter/pipeline.go
+autonomous: true
+requirements: [WAV-01, WAV-02, WAV-03, WAV-04, WAV-05]
+
+must_haves:
+ truths:
+ - "writeWavTags() writes ID3v2 metadata to a WAV file via a RIFF id3 chunk"
+ - "All non-ID3v2 RIFF chunks are preserved byte-for-byte in original order"
+ - "RF64 files are rejected with a clear error message"
+ - "Files >4GB after write are rejected before writing"
+ - "Existing ID3v2 tags in the WAV are merged (unknown frames preserved)"
+ - "Album artist is mapped to TPE2 for all ID3v2 writers (MP3 and WAV)"
+ - "DetectFormat returns FormatWAV for .wav files"
+ - "Pipeline dispatches to writeWavTags for WAV format"
+ artifacts:
+ - path: "backend/tagwriter/wav.go"
+ provides: "RIFF chunk parser, RIFF writer, writeWavTags function"
+ exports: ["writeWavTags"]
+ - path: "backend/tagwriter/tagwriter.go"
+ provides: "FormatWAV constant, .wav case in DetectFormat"
+ contains: "FormatWAV"
+ - path: "backend/tagwriter/mp3.go"
+ provides: "TPE2 album_artist mapping in applyTextChanges"
+ contains: "FieldAlbumArtist"
+ - path: "backend/tagwriter/pipeline.go"
+ provides: "FormatWAV dispatch case in WriteTrackTags"
+ contains: "FormatWAV"
+ key_links:
+ - from: "backend/tagwriter/pipeline.go"
+ to: "backend/tagwriter/wav.go"
+ via: "case FormatWAV in format dispatch switch"
+ pattern: "case FormatWAV.*writeWavTags"
+ - from: "backend/tagwriter/wav.go"
+ to: "backend/tagwriter/mp3.go"
+ via: "reuse applyTextChanges and applyCoverArtChanges"
+ pattern: "applyTextChanges|applyCoverArtChanges"
+ - from: "backend/tagwriter/wav.go"
+ to: "backend/fileutil/atomicwrite.go"
+ via: "fileutil.AtomicWrite for crash-safe writes"
+ pattern: "fileutil\\.AtomicWrite"
+---
+
+
+Implement the WAV tag writer: a custom RIFF chunk parser/writer that preserves all non-ID3v2 chunks byte-for-byte and embeds ID3v2 metadata via the `id3 ` chunk, using `bogem/id3v2` for tag manipulation and `fileutil.AtomicWrite` for crash safety.
+
+Purpose: Enable WAV files to be edited with the same metadata pipeline as MP3/FLAC — this is the core backend capability for Phase 19.
+Output: `wav.go` with RIFF parser + writer + writeWavTags; updated `tagwriter.go` + `pipeline.go` with FormatWAV support; fixed `mp3.go` with album_artist TPE2 mapping.
+
+
+
+@/home/caleb/.config/opencode/get-shit-done/workflows/execute-plan.md
+@/home/caleb/.config/opencode/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/19-wav-tag-writer/19-RESEARCH.md
+@.planning/phases/19-wav-tag-writer/19-CONTEXT.md
+
+
+
+
+From backend/tagwriter/tagwriter.go:
+```go
+type TagChanges map[string]any
+
+const (
+ FieldTitle = "title"
+ FieldArtist = "artist"
+ FieldAlbum = "album"
+ FieldAlbumArtist = "album_artist"
+ FieldGenre = "genre"
+ FieldYear = "year"
+ FieldTrackNumber = "track_number"
+ FieldDiscNumber = "disc_number"
+ FieldComposer = "composer"
+ FieldCoverArt = "cover_art"
+)
+
+type AudioFormat string
+
+const (
+ FormatMP3 AudioFormat = "mp3"
+ FormatFLAC AudioFormat = "flac"
+)
+
+func DetectFormat(filePath string) (AudioFormat, error)
+func asInt(v any) (int, bool)
+func asBytes(v any) ([]byte, bool)
+func detectMIME(data []byte) string
+```
+
+From backend/tagwriter/mp3.go:
+```go
+func writeMp3Tags(logger *slog.Logger, filePath string, changes TagChanges) error
+func applyTextChanges(tag *id3v2.Tag, changes TagChanges) // reuse for WAV
+func applyCoverArtChanges(tag *id3v2.Tag, changes TagChanges) // reuse for WAV
+func copyAudioData(originalPath string, tagSize int64, dst *os.File) error
+```
+
+From backend/tagwriter/pipeline.go:
+```go
+// Format dispatch switch (lines 142-149):
+switch format {
+case FormatMP3:
+ err = writeMp3Tags(tw.logger, audioFile.FilePath, changes)
+case FormatFLAC:
+ err = writeFlacTags(tw.logger, audioFile.FilePath, changes)
+default:
+ err = fmt.Errorf("%w: %s", errUnsupportedFormat, format)
+}
+```
+
+From backend/fileutil/atomicwrite.go:
+```go
+func AtomicWrite(logger *slog.Logger, targetPath string, fn func(tmp *os.File) error) (err error)
+```
+
+From backend/tagwriter/flac.go (pattern reference):
+```go
+func writeFlacTags(logger *slog.Logger, filePath string, changes TagChanges) error
+// Pattern: parse file → modify metadata → AtomicWrite callback → f.WriteTo(tmp)
+// Warns above 500MB via slog
+```
+
+
+
+
+
+
+ Task 1: Fix album_artist TPE2 mapping in shared applyTextChanges
+ backend/tagwriter/mp3.go
+
+Add the missing `FieldAlbumArtist` → TPE2 mapping to `applyTextChanges()` in mp3.go. This fixes a latent gap in MP3 writing AND enables WAV to reuse the same function. Insert after the `FieldComposer` block (around line 83):
+
+```go
+if v, ok := changes[FieldAlbumArtist].(string); ok {
+ tpe2ID := tag.CommonID("Band/Orchestra/Accompaniment")
+ tag.DeleteFrames(tpe2ID)
+ tag.AddTextFrame(tpe2ID, id3v2.EncodingUTF8, v)
+}
+```
+
+This follows the exact same pattern as the existing FieldComposer → TCOM mapping. The `CommonID("Band/Orchestra/Accompaniment")` resolves to "TPE2" which is the standard ID3v2 frame for album artist.
+
+Also verify the line length stays under 100 chars (golines linter). Break the tag.AddTextFrame call across lines if needed.
+
+
+ go test -tags webkit2_41 -run TestWriteMp3Tags ./backend/tagwriter/ -count=1
+
+ applyTextChanges handles FieldAlbumArtist via TPE2 frame; existing MP3 tests still pass
+
+
+
+ Task 2: Create WAV RIFF parser/writer and writeWavTags function
+ backend/tagwriter/wav.go, backend/tagwriter/tagwriter.go, backend/tagwriter/pipeline.go
+
+**Step 1: Add FormatWAV to tagwriter.go**
+
+Add the WAV format constant after FormatFLAC:
+```go
+// FormatWAV is the WAV audio format.
+FormatWAV AudioFormat = "wav"
+```
+
+Add `.wav` case to DetectFormat switch:
+```go
+case ".wav":
+ return FormatWAV, nil
+```
+
+**Step 2: Add dispatch case to pipeline.go**
+
+In the WriteTrackTags format switch (after `case FormatFLAC:`), add:
+```go
+case FormatWAV:
+ err = writeWavTags(tw.logger, audioFile.FilePath, changes)
+```
+
+**Step 3: Create wav.go with RIFF parser/writer and writeWavTags**
+
+Create `backend/tagwriter/wav.go` with:
+
+1. Package doc comment, imports (`bytes`, `encoding/binary`, `errors`, `fmt`, `io`, `log/slog`, `os`, `github.com/bogem/id3v2/v2`, `yellowjacket/backend/fileutil`)
+
+2. Sentinel errors:
+ - `errRF64NotSupported = errors.New("RF64 files are not yet supported")`
+ - `errNotRIFF = errors.New("not a RIFF file")`
+ - `errNotWAVE = errors.New("not a WAVE file")`
+ - `errFileTooLargeForWAV = errors.New("file too large for WAV format (>4GB)")`
+
+3. `riffChunk` struct: `type riffChunk struct { id [4]byte; data []byte }`
+
+4. `parseRIFF(r io.ReadSeeker) ([]riffChunk, error)`:
+ - Read 4-byte magic. If `RF64` → return errRF64NotSupported. If not `RIFF` → return errNotRIFF.
+ - Read 4-byte uint32 LE riffSize (read but don't enforce — lenient read per user decision).
+ - Read 4-byte form type. If not `WAVE` → return errNotWAVE.
+ - Loop reading chunks until EOF:
+ - Read 4-byte chunk ID + 4-byte uint32 LE chunk size.
+ - Read `chunkSize` bytes of data via `io.ReadFull`.
+ - Append `riffChunk{id, data}`.
+ - If `chunkSize` is odd, skip 1 padding byte (lenient: if read fails, break — don't error).
+ - Return chunks slice.
+
+5. `isID3ChunkID(id [4]byte) bool`:
+ - Case-insensitive check: returns true if first 3 bytes are 'i','d','3' or 'I','D','3' (accept both `id3 ` and `ID3 ` per research).
+
+6. `writeRIFF(w io.Writer, chunks []riffChunk, id3Data []byte) error`:
+ - Calculate total RIFF data size: `4` (WAVE) + for each chunk: `8 + len(c.data) + padding`. For `id3 ` chunk: `8 + len(id3Data) + padding`.
+ - Check 4GB limit: if total + 8 > 0xFFFFFFFF, return errFileTooLargeForWAV.
+ - Write `RIFF` + uint32 LE total size + `WAVE`.
+ - Write each preserved chunk: ID + uint32 LE len(data) + data + padding byte if odd.
+ - Write `id3 ` chunk: `id3 ` + uint32 LE len(id3Data) + id3Data + padding byte if odd.
+ - Return nil on success.
+
+7. `writeWavTags(logger *slog.Logger, filePath string, changes TagChanges) error`:
+ - Stat file. If size > 500MB, log warning via slog (same threshold as FLAC writer).
+ - Open file for reading, defer close.
+ - Call `parseRIFF(f)`. On error, return friendly wrapped error.
+ - Separate chunks: iterate all chunks, collect non-ID3 chunks into `preserved` slice and extract ID3 data from any `id3 ` chunk (using `isID3ChunkID`).
+ - Build ID3v2 tag:
+ - If existing ID3 data found: `id3v2.ParseReader(bytes.NewReader(existingID3), id3v2.Options{Parse: true})`
+ - If no existing ID3 data: `id3v2.NewEmptyTag()` with `tag.SetDefaultEncoding(id3v2.EncodingUTF8)`
+ - Apply changes: `applyTextChanges(tag, changes)` + `applyCoverArtChanges(tag, changes)` (reusing MP3 functions).
+ - Serialize tag: `var id3Buf bytes.Buffer` → `tag.WriteTo(&id3Buf)`.
+ - Close the source file (release handle before AtomicWrite).
+ - Call `fileutil.AtomicWrite(logger, filePath, func(tmp *os.File) error { return writeRIFF(tmp, preserved, id3Buf.Bytes()) })`.
+
+**Critical implementation details (from CONTEXT.md locked decisions):**
+- Preserve ALL non-ID3v2 chunks byte-for-byte in original order.
+- ID3v2 chunk placed at END of file.
+- Read and merge existing ID3v2 tags (preserve unknown frames from other tools).
+- Lenient read (accept missing padding, ignore RIFF size mismatch), strict write (correct padding, correct sizes).
+- Write `id3 ` (lowercase) chunk ID.
+- Accept both `id3 ` and `ID3 ` on read.
+
+**Linting requirements:**
+- All doc comments end with period (godot).
+- Sentinel errors as package-level vars (err113).
+- Blank line after early returns (nlreturn).
+- Lines under 100 chars (golines).
+- No cuddled var declarations (wsl).
+- Import groups: stdlib, third-party, internal (gci).
+
+
+ go build -tags webkit2_41 ./backend/tagwriter/ && go vet -tags webkit2_41 ./backend/tagwriter/
+
+ wav.go exists with parseRIFF, writeRIFF, writeWavTags; tagwriter.go has FormatWAV constant and .wav DetectFormat case; pipeline.go dispatches FormatWAV to writeWavTags; package compiles and vets clean
+
+
+
+
+
+- `go build -tags webkit2_41 ./backend/tagwriter/` succeeds
+- `go vet -tags webkit2_41 ./backend/tagwriter/` has no issues
+- `go test -tags webkit2_41 -run TestWriteMp3Tags ./backend/tagwriter/ -count=1` passes (existing tests not broken)
+- `go test -tags webkit2_41 -run TestWriteFlacTags ./backend/tagwriter/ -count=1` passes (existing tests not broken)
+
+
+
+- FormatWAV is detected for `.wav` extension
+- writeWavTags compiles and is reachable from pipeline dispatch
+- RIFF parser handles: valid WAV, RF64 rejection, non-RIFF rejection
+- RIFF writer produces correct chunk structure with padding
+- Existing MP3 and FLAC tests pass (no regressions)
+- applyTextChanges handles album_artist via TPE2
+
+
+
diff --git a/.planning/phases/19-wav-tag-writer/19-02-PLAN.md b/.planning/phases/19-wav-tag-writer/19-02-PLAN.md
new file mode 100644
index 0000000..9f53b57
--- /dev/null
+++ b/.planning/phases/19-wav-tag-writer/19-02-PLAN.md
@@ -0,0 +1,320 @@
+---
+phase: 19-wav-tag-writer
+plan: 02
+type: execute
+wave: 2
+depends_on: ["19-01"]
+files_modified:
+ - backend/tagwriter/wav_test.go
+autonomous: true
+requirements: [WAV-01, WAV-02, WAV-03, WAV-04, WAV-05, WAV-06]
+
+must_haves:
+ truths:
+ - "WAV text fields round-trip: write 8 fields → read back all 8 with correct values"
+ - "WAV cover art round-trip: embed JPEG → read back identical bytes and MIME type"
+ - "WAV clear cover art: embed then clear → no picture data on read-back"
+ - "WAV partial update: change 2 of 8 fields → other 6 fields preserved"
+ - "WAV chunk preservation: non-ID3v2 chunks (fmt, data, LIST INFO, bext) survive tag write unchanged"
+ - "WAV atomic safety: failed write leaves original file untouched"
+ - "RF64 files are rejected with clear error"
+ - "All tests pass via make test (no regressions across entire suite)"
+ artifacts:
+ - path: "backend/tagwriter/wav_test.go"
+ provides: "createTestWAV fixture builder, readWavID3Tags read-back helper, 7+ test functions"
+ min_lines: 200
+ key_links:
+ - from: "backend/tagwriter/wav_test.go"
+ to: "backend/tagwriter/wav.go"
+ via: "calls writeWavTags, parseRIFF, isID3ChunkID"
+ pattern: "writeWavTags|parseRIFF|isID3ChunkID"
+ - from: "backend/tagwriter/wav_test.go"
+ to: "backend/tagwriter/helpers_test.go"
+ via: "uses tinyJPEG, testLogger, assertEqual, assertStrField, assertIntField"
+ pattern: "tinyJPEG|testLogger|assertEqual|assertStrField|assertIntField"
+---
+
+
+Create comprehensive round-trip tests for the WAV tag writer, verifying all 6 WAV requirements via automated tests that mirror the existing MP3 and FLAC test patterns.
+
+Purpose: Prove that WAV tag writing works correctly for all fields, cover art, partial updates, chunk preservation, and atomic safety — completing the WAV-06 requirement.
+Output: `wav_test.go` with test fixture builder, read-back helper, and 7+ test functions.
+
+
+
+@/home/caleb/.config/opencode/get-shit-done/workflows/execute-plan.md
+@/home/caleb/.config/opencode/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/19-wav-tag-writer/19-RESEARCH.md
+@.planning/phases/19-wav-tag-writer/19-CONTEXT.md
+@.planning/phases/19-wav-tag-writer/19-01-SUMMARY.md
+
+
+
+
+From backend/tagwriter/wav.go (created in Plan 01):
+```go
+type riffChunk struct {
+ id [4]byte
+ data []byte
+}
+
+func parseRIFF(r io.ReadSeeker) ([]riffChunk, error)
+func isID3ChunkID(id [4]byte) bool
+func writeRIFF(w io.Writer, chunks []riffChunk, id3Data []byte) error
+func writeWavTags(logger *slog.Logger, filePath string, changes TagChanges) error
+```
+
+From backend/tagwriter/tagwriter.go:
+```go
+type TagChanges map[string]any
+const FieldTitle, FieldArtist, FieldAlbum, FieldAlbumArtist, FieldGenre = ...
+const FieldYear, FieldTrackNumber, FieldDiscNumber, FieldComposer, FieldCoverArt = ...
+```
+
+From backend/tagwriter/mp3.go (shared functions used by writeWavTags):
+```go
+func applyTextChanges(tag *id3v2.Tag, changes TagChanges) // includes TPE2 album_artist
+func applyCoverArtChanges(tag *id3v2.Tag, changes TagChanges)
+```
+
+From backend/tagwriter/helpers_test.go:
+```go
+func testLogger() *slog.Logger
+func tinyJPEG(t *testing.T) []byte
+func makeMinimalJPEG(t *testing.T) []byte
+func assertEqual[T comparable](t *testing.T, field string, want, got T)
+func assertStrField(t *testing.T, name, got, want string)
+func assertIntField(t *testing.T, name string, got, want int)
+```
+
+From backend/metadata/tags.go (TrackMetadata struct for test assertions):
+```go
+type TrackMetadata struct {
+ Title, Artist, Album, AlbumArtist, Composer, Genre string
+ Year, TrackNumber, DiscNumber int
+ Picture *PictureData
+}
+type PictureData struct {
+ Data []byte; MIMEType string; Ext string
+}
+```
+
+Test pattern reference — from backend/tagwriter/mp3_test.go:
+```go
+func createTestMP3(t *testing.T, dir string, name string, fields TagChanges) string
+// Creates minimal MP3: id3v2.NewEmptyTag() + applyTextChanges + WriteTo + MPEG frame
+// Tests: TextFields, CoverArt, ClearCoverArt, PartialUpdate, AtomicSafety
+// Read-back: metadata.ExtractTags(path) → assert fields
+```
+
+Test pattern reference — from backend/tagwriter/flac_test.go:
+```go
+func makeMinimalFLAC(t *testing.T, path string)
+// Tests: TextFields, CoverArt, ClearCoverArt, PartialUpdate, PreservesStreamInfo, ReplaceComment, AtomicSafety
+// Read-back: metadata.ExtractTags(path) → assert fields
+```
+
+CRITICAL NOTE from RESEARCH.md:
+```
+dhowden/tag does NOT support WAV files — tag.ReadFrom() returns ErrNoTagsFound.
+Tests CANNOT use metadata.ExtractTags() for WAV read-back.
+Must extract id3 chunk from RIFF, then use tag.ReadID3v2Tags() on extracted bytes.
+```
+
+
+
+
+
+
+ Task 1: Create WAV test fixture builder and read-back helper
+ backend/tagwriter/wav_test.go
+
+Create `backend/tagwriter/wav_test.go` with package `tagwriter` (internal test — matches mp3_test.go and flac_test.go pattern).
+
+**Imports needed:** `bytes`, `encoding/binary`, `os`, `path/filepath`, `testing`, `github.com/bogem/id3v2/v2`, `github.com/dhowden/tag`, `yellowjacket/backend/metadata` (for TrackMetadata/PictureData types).
+
+**1. createTestWAV(t *testing.T, dir, name string, fields TagChanges) string**
+
+Builds a minimal valid WAV file programmatically (same pattern as createTestMP3):
+
+```
+RIFF header (12 bytes):
+ "RIFF" + uint32 LE total_data_size + "WAVE"
+
+fmt chunk (24 bytes):
+ "fmt " + uint32(16) + 16 bytes PCM format:
+ AudioFormat=1 (PCM), NumChannels=1, SampleRate=44100,
+ ByteRate=88200, BlockAlign=2, BitsPerSample=16
+
+data chunk (208 bytes):
+ "data" + uint32(200) + 200 bytes of silence (zeros)
+
+id3 chunk (if fields provided):
+ "id3 " + uint32(len) + id3v2 tag bytes + padding if odd
+```
+
+Build the ID3v2 tag using `id3v2.NewEmptyTag()` + `tag.SetDefaultEncoding(id3v2.EncodingUTF8)` + `applyTextChanges(tag, fields)` + `applyCoverArtChanges(tag, fields)` + `tag.WriteTo(&id3Buf)`.
+
+Calculate riffDataSize: 4 (WAVE) + 24 (fmt) + 208 (data) + id3ChunkSize. Write all chunks, then write file to `filepath.Join(dir, name)` with `os.WriteFile(path, buf.Bytes(), 0o644)`. Return path.
+
+**2. readWavID3Tags(t *testing.T, path string) *metadata.TrackMetadata**
+
+Test helper that extracts ID3v2 metadata from a WAV file for read-back verification:
+
+- Open file, call `parseRIFF(f)`.
+- Find the chunk where `isID3ChunkID(c.id)` is true.
+- Call `tag.ReadID3v2Tags(bytes.NewReader(c.data))` (from `github.com/dhowden/tag`).
+- Convert `tag.Metadata` to `*metadata.TrackMetadata`:
+ - `m.Title()`, `m.Artist()`, `m.Album()`, `m.AlbumArtist()`, `m.Composer()`, `m.Genre()`, `m.Year()`
+ - `m.Track()` → trackNum, `m.Disc()` → discNum
+ - `m.Picture()` → PictureData if non-nil
+- Return the TrackMetadata.
+- If no id3 chunk found, `t.Fatal("no id3 chunk found in WAV file")`.
+
+**3. createTestWAVWithExtraChunks(t *testing.T, dir, name string) string**
+
+Creates a WAV file with additional non-standard chunks to test chunk preservation:
+
+Build the same base WAV as createTestWAV (no fields), but insert these additional chunks between fmt and data:
+- LIST INFO chunk: `LIST` + size + `INFO` + `INAM` sub-chunk with "Test Track Name"
+- A fake `bext` chunk: `bext` + 8 bytes of test data
+
+This tests that writeWavTags preserves chunks it doesn't understand.
+
+All test helpers call `t.Helper()` at the start.
+
+
+ go build -tags webkit2_41 ./backend/tagwriter/
+
+ wav_test.go compiles with createTestWAV, readWavID3Tags, and createTestWAVWithExtraChunks helpers
+
+
+
+ Task 2: Write round-trip tests for all WAV requirements
+ backend/tagwriter/wav_test.go
+
+Add the following test functions to `wav_test.go`, mirroring the MP3/FLAC test patterns:
+
+**TestWriteWavTags_TextFields** (covers WAV-01):
+- Create bare WAV fixture with `createTestWAV(t, dir, "text.wav", nil)`.
+- Write all 8+1 text fields: Title, Artist, Album, AlbumArtist, Genre, Year(2024), TrackNumber(3), DiscNumber(1), Composer.
+- Call `writeWavTags(testLogger(), path, changes)`.
+- Read back with `readWavID3Tags(t, path)`.
+- Assert all 9 fields match using `assertStrField`/`assertIntField`.
+
+**TestWriteWavTags_CoverArt** (covers WAV-04):
+- Create bare WAV, write cover art via `FieldCoverArt: tinyJPEG(t)`.
+- Read back, assert Picture is non-nil, `bytes.Equal` on data, MIME is "image/jpeg".
+
+**TestWriteWavTags_ClearCoverArt** (covers WAV-04):
+- Create WAV with cover art embedded via `createTestWAV(t, dir, "clear.wav", TagChanges{FieldCoverArt: art})`.
+- Verify art present.
+- Write with `TagChanges{FieldCoverArt: nil}` to clear.
+- Read back, assert Picture is nil.
+
+**TestWriteWavTags_PartialUpdate** (covers WAV-01):
+- Create WAV with all fields populated via createTestWAV.
+- Write only Title and Artist changes.
+- Read back, assert Title and Artist changed, all other 7 fields preserved.
+
+**TestWriteWavTags_ChunkPreservation** (covers WAV-02, WAV-03):
+- Create WAV with extra chunks via `createTestWAVWithExtraChunks`.
+- Read original file bytes.
+- Write tags (Title only).
+- Re-read file, parse RIFF chunks.
+- Assert: fmt chunk data is byte-identical to original; data chunk data is byte-identical to original (WAV-03: audio data preserved); LIST chunk still present with original data; bext chunk still present with original data.
+- Count total non-id3 chunks: should match original count.
+
+**TestWriteWavTags_AtomicSafety** (covers WAV-05):
+- Create WAV with initial tags.
+- Read original file bytes.
+- Attempt `writeWavTags` to a non-existent directory path (forces AtomicWrite to fail on temp file creation).
+- Assert error returned.
+- Read file bytes, assert identical to original (no modification on failure).
+
+**TestWriteWavTags_RejectsRF64** (covers WAV-02 edge case):
+- Create a file that starts with `RF64` + 4 size bytes + `WAVE` + minimal chunks.
+- Attempt `writeWavTags`.
+- Assert error contains "RF64".
+
+**Linting requirements:**
+- `t.Parallel()` at both suite and subtest level where possible (no shared state between tests since each uses t.TempDir()).
+- `t.Helper()` in all helper functions.
+- `t.Fatalf` for setup failures, `t.Errorf` for assertion failures.
+- `//nolint:mnd` on magic numbers in fixture construction.
+- Lines under 100 chars.
+- Blank lines after early returns.
+
+
+ go test -tags webkit2_41 -run TestWriteWavTags -v ./backend/tagwriter/ -count=1
+
+ All 7 WAV test functions pass; text fields round-trip, cover art round-trip, clear cover art, partial update, chunk preservation, atomic safety, and RF64 rejection all verified
+
+
+
+ Task 3: Full test suite verification
+
+
+Run the full project test suite to verify no regressions:
+
+```bash
+make test
+```
+
+This runs `go test -tags webkit2_41 -race -count=1 -timeout 120s ./...` which covers:
+- All existing MP3 tag writer tests
+- All existing FLAC tag writer tests
+- All new WAV tag writer tests
+- Pipeline tests
+- Metadata tests
+- All other package tests
+
+Also run the lint check:
+```bash
+make lint
+```
+
+If any test failures or lint warnings exist:
+- Fix WAV-specific issues in wav.go or wav_test.go.
+- If existing test failures are unrelated to WAV changes, note them but don't modify unrelated code.
+
+If lint warnings in wav.go or wav_test.go:
+- Fix them (golines, nlreturn, wsl, godot, err113, etc.).
+
+
+ make test && make lint
+
+ make test passes with 0 failures; make lint passes with 0 warnings in tagwriter package; WAV-06 requirement (round-trip tests) is fully satisfied
+
+
+
+
+
+- `go test -tags webkit2_41 -run TestWriteWavTags -v ./backend/tagwriter/ -count=1` — all 7 WAV tests pass
+- `go test -tags webkit2_41 -run TestWriteMp3Tags ./backend/tagwriter/ -count=1` — MP3 tests still pass (no regression)
+- `go test -tags webkit2_41 -run TestWriteFlacTags ./backend/tagwriter/ -count=1` — FLAC tests still pass (no regression)
+- `make test` — full test suite passes
+- `make lint` — no lint warnings in tagwriter package
+
+
+
+- All 8 text fields round-trip correctly (Title, Artist, Album, AlbumArtist, Genre, Year, TrackNumber, DiscNumber, Composer)
+- Cover art embed/replace/clear works
+- Partial updates preserve unchanged fields
+- Non-ID3v2 RIFF chunks preserved byte-for-byte
+- Audio data (data chunk) preserved byte-for-byte
+- Atomic write leaves original untouched on failure
+- RF64 rejected with clear error
+- Full test suite (make test) passes with zero failures
+- Lint (make lint) passes with zero warnings
+
+
+