Files
suaveplan/openspec/specs/audio-metadata/spec.md
2026-05-27 23:29:53 +01:00

25 KiB
Raw Blame History

Capability Spec: audio-metadata

Package: @suaveplan/audio-metadata Status: STABLE Tier: universal

Every requirement below carries a **Verified by:** line anchored to a real test in packages/media/audio/audio-metadata/. No exceptions.

@suaveplan/audio-metadata is the universal audio-metadata reader: it detects audio container formats by magic bytes, dispatches to a format-specific parser, and normalises container-native tags (ID3v1, ID3v2, iTunes ilst, Vorbis comments, RIFF INFO, AIFF/ID3) into a single Track shape. The package is environment-agnostic (Node ≥18, browser, Deno), zero-deps from @suaveplan/*, and never throws on malformed buffers — parsers return partial Track objects for unrecognised or truncated input.

Requirements

Requirement: Universal-tier purity with zero workspace runtime deps

The package SHALL import zero browser-only APIs (window, document, navigator), zero Node-only APIs (node:fs, node:path, process), and SHALL NOT depend on any @suaveplan/* workspace package at runtime. The only non-workspace runtime dependency permitted is @opentelemetry/api, loaded dynamically and gracefully absent on failure. tslib is permitted as a build helper.

Scenario: dependency closure contains no @suaveplan/* runtime entry

  • WHEN package.json dependencies is inspected
  • THEN no key starts with @suaveplan/

Scenario: built dist references no platform globals

  • WHEN dist/ is grep'd for window, document, node:fs, node:path, process, AudioContext, Buffer
  • THEN the count is zero

Verified by: tests/dep-graph.test.ts, tests/no-disallowed.test.ts


Requirement: Eight container-format detection via magic-byte sniffing

The package SHALL export detectAudioFormat(buffer: Uint8Array): AudioFormat returning one of "id3v2" | "id3v1" | "mp4" | "flac" | "ogg" | "wav" | "aiff" | "unknown". Detection SHALL probe in this order so a file carrying both ID3v2 header and ID3v1 trailer resolves as "id3v2": (1) ID3v2 at byte 0 (ID3); (2) FLAC at byte 0 (fLaC); (3) Ogg at byte 0 (OggS); (4) WAV (RIFF at 0 AND WAVE at 8); (5) AIFF (FORM at 0 AND AIFF or AIFC at 8); (6) MP4 (ftyp or moov at byte 4); (7) ID3v1 trailer (TAG at offset length - 128); (8) "unknown".

Scenario: ID3v2 detected before ID3v1 when both present

  • WHEN a buffer carries ID3 at byte 0 and TAG at length-128
  • THEN detectAudioFormat returns "id3v2"

Scenario: WAV requires both RIFF and WAVE

  • WHEN the buffer starts with RIFF but byte 8 onwards is not WAVE
  • THEN detectAudioFormat does not return "wav"

Scenario: AIFC accepted as AIFF variant

  • WHEN a buffer carries FORM at byte 0 and AIFC at byte 8
  • THEN detectAudioFormat returns "aiff"

Scenario: empty or short buffer returns unknown

  • WHEN detectAudioFormat(new Uint8Array(0)) is called
  • THEN the return value is "unknown"

Verified by: src/parsers/format-detector/format-detector.test.ts (every detection test)


Requirement: Track normalisation across all tag families

The package SHALL normalise tags from every supported container into a single Track interface with optional fields title, artist, album, albumArtist, year (number), trackNumber, totalTracks, discNumber, totalDiscs, genre, comment, lyrics, cover: CoverArt, duration (seconds), bitrate (kbps), sampleRate (Hz), channels, format: AudioFormat, rawTags: Record<string, string | string[]>. Absent fields SHALL use undefined, not null. Tags carrying multiple values (Vorbis comment, TXXX) SHALL be preserved as string[] in rawTags while normalised track.* fields take the first value.

Scenario: ID3v2 TRCK maps to trackNumber and totalTracks

  • WHEN parseId3v2 reads a TRCK frame value "3/12"
  • THEN track.trackNumber === 3 and track.totalTracks === 12

Scenario: MP4 trkn 6-byte structure maps correctly

  • WHEN parseMp4 reads a trkn atom whose data is [0, 0, 0, 5, 0, 10] (2 padding + 2 number + 2 total, big-endian)
  • THEN track.trackNumber === 5 and track.totalTracks === 10

Scenario: Vorbis multi-value tags preserved as array

  • WHEN parseVorbisCommentBlock reads two ARTIST= entries
  • THEN track.artist equals the first value and track.rawTags.ARTIST is the array

Verified by: src/parsers/id3v2/id3v2.test.ts::"parses track number (TRCK)", src/parsers/mp4-atom/mp4-atom.test.ts::"parses trkn (track number)", src/parsers/flac-vorbis/flac-vorbis.test.ts::"parses basic Vorbis comment tags"


Requirement: ID3v1 / ID3v1.1 parsing

parseId3v1(buffer: Uint8Array): Track SHALL parse the fixed 128-byte tag at buffer.length - 128. It SHALL extract title (30 bytes), artist (30), album (30), year (4 numeric chars or undefined if non-numeric), comment (28 or 30 bytes), and genre (1 byte index into the 192-entry Winamp-extended list). It SHALL detect ID3v1.1 track-number extension via the comment byte-28 = 0 sentinel and store the trailing byte as track.trackNumber. Strings SHALL be decoded as Latin-1 (ISO-8859-1) and stripped of trailing null + whitespace via regex. Returns {} when TAG magic is absent or buffer is shorter than 128 bytes.

Scenario: ID3v1.1 track number recognised

  • WHEN a 128-byte tag has byte 28 = 0x00 and byte 29 ≠ 0
  • THEN track.trackNumber is byte 29 and track.comment is the 28-byte slice

Scenario: non-numeric year ignored

  • WHEN the four year bytes contain non-digit characters
  • THEN track.year is undefined

Scenario: empty result for missing magic or short buffer

  • WHEN the buffer is shorter than 128 bytes or TAG magic is absent
  • THEN parser returns {}

Verified by: src/parsers/id3v1/id3v1.test.ts::"parses track number (ID3v1.1)", src/parsers/id3v1/id3v1.test.ts::"ignores non-numeric year string", src/parsers/id3v1/id3v1.test.ts::"returns empty track when buffer too short"


Requirement: ID3v2.2 / 2.3 / 2.4 parsing with full text encoding support

parseId3v2(buffer, options?) SHALL parse ID3v2 tags at byte 0 supporting all three minor versions (v2.2, v2.3, v2.4). Frame IDs from v2.2 (TT2, TP1, TAL, TRK, TYE, TCO, COM, ULT, PIC) SHALL be normalised to their v2.3/v2.4 equivalents (TIT2, TPE1, TALB, TRCK, TYER, TCON, COMM, USLT, APIC). The parser SHALL decode all four text encodings: ISO-8859-1 (0x00), UTF-16 with BOM (0x01), UTF-16 BE without BOM (0x02), UTF-8 (0x03). It SHALL handle whole-tag unsync flag (v2.2 / v2.3), extended headers (v2.3+), and syncsafe size integers (4 × 7 bits = 28 bits, 256 MiB max). Numeric genre references in TCON (e.g. (17), 17, (17)Rock) SHALL be resolved against the Winamp-extended list and concatenated with any free text suffix.

Scenario: TIT2 frame parsed as title

  • WHEN a tag carries a UTF-8 TIT2 frame with "Song"
  • THEN track.title === "Song"

Scenario: TDRC ISO-date year parsed

  • WHEN the tag carries a v2.4 TDRC frame with "2023-04-15"
  • THEN track.year === 2023

Scenario: skipCovers option suppresses APIC extraction

  • WHEN caller passes { skipCovers: true }
  • THEN track.cover is undefined even if an APIC frame is present

Scenario: skipLyrics option suppresses USLT extraction

  • WHEN caller passes { skipLyrics: true }
  • THEN track.lyrics is undefined even if a USLT frame is present

Verified by: src/parsers/id3v2/id3v2.test.ts::"parses title frame (TIT2)", src/parsers/id3v2/id3v2.test.ts::"parses year from TDRC (ISO date)", src/parsers/id3v2/id3v2.test.ts::"respects skipCovers option", src/parsers/id3v2/id3v2.test.ts::"respects skipLyrics option"


Requirement: MP4 iTunes ilst atom parsing

parseMp4(buffer, options?) SHALL traverse the MP4 box hierarchy moov → udta → meta → ilst (and the fallback moov → meta → ilst path some encoders use) and decode iTunes atoms with the prefixes ©nam (title), ©ART (artist), ©alb (album), aART (album artist), ©day (year), ©cmt (comment), ©lyr (lyrics), ©gen (text genre), gnre (numeric genre index, 1-based into ID3v1 list), trkn (track 6-byte structure), disk (disc 6-byte structure), covr (embedded artwork). 64-bit extended boxes (size sentinel = 1) SHALL be skipped without crashing. Returns {} if no moov box is present; returns { format: "mp4" } if moov exists but no ilst.

Scenario: ©day year parsed

  • WHEN a buffer carries an ©day atom with "2024"
  • THEN track.year === 2024

Scenario: covr atom MIME type detection

  • WHEN the covr data atom flag is 0x0d
  • THEN track.cover.mimeType === "image/jpeg"
  • WHEN the flag is 0x0e
  • THEN track.cover.mimeType === "image/png"

Scenario: gnre index 0 ignored

  • WHEN the gnre atom encodes index 0
  • THEN track.genre is undefined (1-based; 0 is invalid)

Verified by: src/parsers/mp4-atom/mp4-atom.test.ts::"parses ©day (year)", src/parsers/mp4-atom/mp4-atom.test.ts::"parses covr (PNG cover art)", src/parsers/mp4-atom/mp4-atom.test.ts::"ignores gnre with index 0 (invalid)"


Requirement: FLAC + Vorbis comment block parsing

parseFlac(buffer) SHALL verify the fLaC magic, walk METADATA_BLOCKs, extract stream properties from STREAMINFO (block type 0): sampleRate (20 bits crossing byte boundaries), channels (3-bit field +1), totalSamples (36 bits = 4-bit high + 32-bit low) for duration computation. It SHALL extract VORBIS_COMMENT (block type 4) via parseVorbisCommentBlock. The exported parseVorbisCommentBlock(data: Uint8Array): Track SHALL parse the raw comment binary (uint32LE vendor length, uint32LE comment count, per-comment uint32LE length + UTF-8 key=value). Recognised keys (case-insensitive, stored uppercase in rawTags): TITLE, ARTIST, ALBUM, ALBUMARTIST, DATE, TRACKNUMBER, TRACKTOTAL/TOTALTRACKS, DISCNUMBER, DISCTOTAL/TOTALDISCS, GENRE, COMMENT, LYRICS/UNSYNCEDLYRICS. Multiple same-key entries SHALL be preserved as string[] in rawTags.

Scenario: duration computed from STREAMINFO totalSamples

  • WHEN STREAMINFO carries sampleRate=44100 and totalSamples=441000
  • THEN track.duration === 10

Scenario: TOTALDISCS aliases DISCTOTAL

  • WHEN Vorbis comment carries TOTALDISCS=2
  • THEN track.totalDiscs === 2

Scenario: block claiming length beyond buffer breaks safely

  • WHEN a metadata block declares a length that extends past the buffer end
  • THEN parsing halts and the partial Track is returned (no throw)

Verified by: src/parsers/flac-vorbis/flac-vorbis.test.ts::"computes duration when streaminfo has non-zero totalSamples", src/parsers/flac-vorbis/flac-vorbis.test.ts::"parses TOTALDISCS alias", src/parsers/flac-vorbis/flac-vorbis.test.ts::"breaks when block claims length beyond buffer"


Requirement: Ogg Vorbis logical-bitstream reconstruction

parseOgg(buffer) SHALL verify OggS magic, walk Ogg pages (27-byte header + segment table + data), and reconstruct logical bitstream packets. It SHALL collect at most three packets (ident, comment, setup); packets are terminated by a segment length < 255. The comment packet SHALL be located by its signature 0x03 + "vorbis". The ident packet (type 0x01) SHALL yield sampleRate (4-byte little-endian at offset 12) and channels (byte 11). When the packet walker fails (truncated segment table or page header), the parser SHALL fall back to a linear scan of the first page data for the comment signature. Returns { format: "ogg" } if magic matches but no comment packet found.

Scenario: sampleRate and channels read from ident packet

  • WHEN an Ogg file's first packet (type 0x01) declares 44_100 Hz / 2 channels
  • THEN track.sampleRate === 44_100 and track.channels === 2

Scenario: fallback linear scan finds comment when walker breaks

  • WHEN the packet walker fails on a multi-page stream but the comment lies on the first page's data
  • THEN the parser recovers it via linear scan

Scenario: short or truncated page returns empty

  • WHEN the buffer is shorter than 27 bytes or a declared page extends past the buffer end
  • THEN the parser returns {} without throwing

Verified by: src/parsers/ogg-vorbis/ogg-vorbis.test.ts::"reads sampleRate and channels from ident packet", src/parsers/ogg-vorbis/ogg-vorbis.test.ts::"uses fallback linear scan when collectOggPackets misses comment", src/parsers/ogg-vorbis/ogg-vorbis.test.ts::"readOggPage returns null when buffer is shorter than 27 bytes (pos+27 > buf.length)"


Requirement: WAV RIFF INFO + fmt chunk parsing

parseWav(buffer) SHALL verify RIFF (offset 0) + WAVE (offset 8) magic and walk RIFF chunks (4-byte FourCC + 4-byte uint32LE size + data; chunks padded to even boundaries). The fmt chunk SHALL yield channels (2LE at +2), sampleRate (4LE at +4), byteRate (4LE at +8) — bitrate derived as (byteRate * 8) / 1000 kbps. The data chunk size, when present after fmt , SHALL yield a duration estimate assuming 16-bit PCM (samples = size / (2 * channels)). A LIST INFO sub-chunk SHALL be walked for INFO fields: INAM → title, IART → artist, IPRD → album, IGNR → genre, ICMT → comment, ICRD → year. Returns {} if magic mismatched or buffer shorter than 12 bytes.

Scenario: bitrate computed from byteRate

  • WHEN fmt chunk declares byteRate = 176_400
  • THEN track.bitrate === 1411

Scenario: LIST without INFO type ignored

  • WHEN a LIST chunk's type is not INFO
  • THEN the LIST is skipped without affecting other fields

Scenario: data chunk before fmt yields no duration

  • WHEN the file has a data chunk but no preceding fmt
  • THEN track.duration is undefined

Verified by: src/parsers/wav-riff/wav-riff.test.ts::"computes bitrate from fmt byteRate", src/parsers/wav-riff/wav-riff.test.ts::"ignores non-INFO LIST chunk", src/parsers/wav-riff/wav-riff.test.ts::"handles data chunk without prior fmt chunk (no duration)"


Requirement: AIFF / AIFC parsing with IEEE 754 80-bit extended float

parseAiff(buffer, options?) SHALL verify FORM (0) + AIFF or AIFC (8) magic and walk AIFF chunks (4-byte FourCC + 4-byte uint32BE size + data; even-byte padding — unlike WAV, AIFF uses big-endian sizes). The COMM chunk SHALL yield channels (2BE at offset 0), numSampleFrames (4BE at offset 2), and sampleRate (80-bit extended IEEE 754 at offset 8: 1 sign bit + 15 exponent bits biased by 16383 + 64-bit explicit mantissa, no implicit 1 bit). The ID3 chunk (also accepts id3 lowercase and the variant with trailing space) SHALL delegate to parseId3v2 with the same options and merge results.

Scenario: COMM IEEE 754 extended decoded to sampleRate

  • WHEN a COMM chunk encodes 44_100 in 80-bit extended form
  • THEN track.sampleRate === 44_100

Scenario: AIFC variant accepted

  • WHEN byte 8 is AIFC instead of AIFF
  • THEN the parser still runs and emits track.format === "aiff"

Scenario: COMM smaller than 18 bytes ignored

  • WHEN the COMM chunk size is less than 18 bytes (insufficient for the full block)
  • THEN the parser skips it without throwing

Scenario: negative sampleRate via IEEE 754 sign bit

  • WHEN the 80-bit extended float has its sign bit set
  • THEN the parser still returns a numeric sampleRate (sign-extended path covered)

Verified by: src/parsers/aiff-id3/aiff-id3.test.ts::"parses COMM chunk for sample rate and channels", src/parsers/aiff-id3/aiff-id3.test.ts::"handles AIFC format", src/parsers/aiff-id3/aiff-id3.test.ts::"ignores COMM chunk smaller than 18 bytes", src/parsers/aiff-id3/aiff-id3.test.ts::"returns negative sample rate via IEEE 754 sign bit (parseIeee754Extended -f path)"


Requirement: Cover-art extraction with size guard

When skipCovers !== true, parsers SHALL extract embedded artwork into track.cover: { data: Uint8Array; mimeType: string; description?: string }. MIME type detection: APIC frames read the in-frame MIME string (defaulting to image/jpeg); MP4 covr atoms use the flag byte (0x0d → JPEG, 0x0e → PNG). The default maxCoverSize SHALL be 5 MiB (5_242_880 bytes); covers larger than this SHALL be dropped silently (returning track.cover === undefined). Callers MAY override via the maxCoverSize option.

Scenario: skipCovers suppresses extraction across parsers

  • WHEN caller passes { skipCovers: true } to extractMetadata or any individual parser
  • THEN track.cover is undefined regardless of input

Scenario: PNG cover art from MP4 detected

  • WHEN an MP4 covr data box has flag 0x0e
  • THEN track.cover.mimeType === "image/png"

Verified by: src/core/extractor/extractor.test.ts::"respects skipCovers option", src/parsers/mp4-atom/mp4-atom.test.ts::"parses covr (PNG cover art)", src/parsers/id3v2/id3v2.test.ts::"respects skipCovers option"


Requirement: Lyrics extraction with skipLyrics opt-out

When skipLyrics !== true, parsers SHALL extract lyrics text into track.lyrics. Sources: ID3v2 USLT frame (after stripping language code + description prefix), MP4 ©lyr atom, Vorbis LYRICS / UNSYNCEDLYRICS keys. The skipLyrics option SHALL suppress extraction across all parsers.

Scenario: USLT extracted by default

  • WHEN an ID3v2 buffer carries a USLT frame
  • THEN track.lyrics contains the parsed text

Scenario: skipLyrics drops MP4 ©lyr

  • WHEN caller passes { skipLyrics: true } to extractor against an MP4 file with ©lyr
  • THEN track.lyrics is undefined

Verified by: src/parsers/id3v2/id3v2.test.ts::"parses USLT (lyrics) frame", src/parsers/id3v2/id3v2.test.ts::"respects skipLyrics option", src/core/extractor/extractor.test.ts::"respects skipLyrics option"


Requirement: MetadataExtractor singleton facade

The package SHALL export MetadataExtractor with getInstance(): MetadataExtractor and instance methods extractFromBuffer(buf, options?): Promise<Track>, extractFromArrayBuffer(buf, options?): Promise<Track>, extractFromFile(file: { arrayBuffer(): Promise<ArrayBuffer> }, options?): Promise<Track>, extractAudioMetadata(buf, options?): Promise<AudioMetadata>. A module-level convenience extractMetadata(buf, options?): Promise<Track> SHALL delegate to the singleton. The extractor SHALL dispatch on detectAudioFormat and wrap parser failures in MetadataError(PARSE_ERROR). extractFromFile SHALL throw MetadataError(FETCH_FAILED) when the file-like object's arrayBuffer() rejects. Unknown formats SHALL throw MetadataError(UNSUPPORTED_FORMAT).

Scenario: singleton identity

  • WHEN MetadataExtractor.getInstance() is called twice
  • THEN both calls return the same reference

Scenario: unknown buffer throws UNSUPPORTED_FORMAT

  • WHEN a random 256-byte buffer with no recognised magic is passed to extractFromBuffer
  • THEN the call rejects with MetadataError(METADATA.UNSUPPORTED_FORMAT)

Scenario: rejecting arrayBuffer() translates to FETCH_FAILED

  • WHEN a Blob-like object whose arrayBuffer() rejects with any value is passed to extractFromFile
  • THEN the call rejects with MetadataError(METADATA.FETCH_FAILED)

Verified by: src/core/extractor/extractor.test.ts::"is a singleton", src/core/extractor/extractor.test.ts::"throws MetadataError for unknown format", src/core/extractor/extractor.test.ts::"extractFromFile throws FETCH_FAILED on rejection", src/core/extractor/extractor.test.ts::"extractFromFile throws FETCH_FAILED when non-Error thrown from arrayBuffer"


Requirement: Stable error code registry under METADATA.* namespace

The package SHALL export METADATA_CODES containing exactly these three codes: METADATA.UNSUPPORTED_FORMAT, METADATA.PARSE_ERROR, METADATA.FETCH_FAILED. The package SHALL export a MetadataError extends Error class with public fields code: MetadataCode and name === "MetadataError". This class deliberately does not extend SuaveplanError so the package can remain a leaf with no @suaveplan/* runtime deps.

Scenario: error code namespacing

  • WHEN caller reads METADATA_CODES.UNSUPPORTED_FORMAT
  • THEN value is exactly "METADATA.UNSUPPORTED_FORMAT"

Scenario: MetadataError name is stable for instanceof-free discrimination

  • WHEN an error is constructed and inspected
  • THEN err.name === "MetadataError" and err.code matches the constructor arg

Verified by: src/core/extractor/extractor.test.ts::"MetadataError has correct code and name"


Requirement: Buffer utility primitives

The package SHALL export readUint8, readUint16BE, readUint16LE, readUint32BE, readUint32LE, readInt32BE, readString(buf, offset, length, encoding?: "utf8" | "latin1"), readSyncsafeInt(buf, offset), concatBuffers(a, b) from ./utils. All read* functions SHALL return 0 (or "" for readString) on out-of-bounds access rather than throwing — trading silent safety for robustness against truncated buffers. readUint32BE SHALL use multiplication by 0x1000000 (not << 24) so high-MSB bytes do not flip to negative. readSyncsafeInt SHALL decode the 4-byte ID3v2 syncsafe form (7 bits per byte, MSB always 0) yielding a 28-bit max of 268_435_455.

Scenario: out-of-bounds read returns 0

  • WHEN readUint16BE(buf, buf.length + 100) is called
  • THEN the return value is 0

Scenario: high MSB byte does not sign-extend

  • WHEN readUint32BE reads a byte sequence whose first byte ≥ 0x80
  • THEN the result is unsigned (≥ 2_147_483_648)

Scenario: syncsafe int decoded

  • WHEN readSyncsafeInt reads [0x00, 0x00, 0x02, 0x01]
  • THEN the result is 257 (0x101 from 7-bit chunks)

Verified by: src/utils/buffer-utils.test.ts (every read* test)


Requirement: OpenTelemetry instrumentation, optional and silent on absence

The extractor SHALL emit OTel telemetry under meter name "@suaveplan/audio-metadata": a counter metadata.extract.total{format} incremented after each successful extraction, and a histogram metadata.extract.duration_ms{format} recording elapsed time via performance.now(). Both instruments SHALL be acquired via dynamic import("@opentelemetry/api") inside the singleton; if the import fails or the API throws, instrumentation SHALL be silently disabled and the extractor MUST still return correct results.

Scenario: instrumentation absent under no-OTel environment

  • WHEN extractFromBuffer runs in a Node environment without @opentelemetry/api installed
  • THEN the result resolves correctly (no thrown error) and no telemetry is emitted

Scenario: format label populated on success

  • WHEN the extractor processes a buffer with ID3v2 magic
  • THEN the counter increment carries { format: "id3v2" }

Verified by: src/core/extractor/extractor.test.ts::"extracts metadata from ID3v2 buffer" (implicitly covers OTel happy-path via mocked OTel registration in tests/setup)


Requirement: AudioMetadata envelope distinct from Track

The package SHALL export extractAudioMetadata(buf, options?): Promise<AudioMetadata> returning { format: AudioFormat; track: Track; duration?: number; bitrate?: number }. The envelope's format and the embedded track.format SHALL agree. The envelope is the only path that surfaces detected format alongside the full normalised Track for callers who want both atomically.

Scenario: envelope format matches track.format

  • WHEN extractAudioMetadata returns successfully against a FLAC buffer
  • THEN result.format === "flac" and result.track.format === "flac"

Verified by: src/core/extractor/extractor.test.ts::"extractAudioMetadata returns AudioMetadata envelope"


Requirement: Subpath exports per parser

The package SHALL expose, in addition to the root barrel, subpath exports for each parser module so adapters can tree-shake unused parsers: ./parsers/id3v1, ./parsers/id3v2, ./parsers/mp4-atom, ./parsers/flac-vorbis, ./parsers/ogg-vorbis, ./parsers/wav-riff, ./parsers/aiff-id3, plus ./errors.

Scenario: per-parser subpath resolves

  • WHEN a consumer imports @suaveplan/audio-metadata/parsers/flac-vorbis
  • THEN resolution succeeds and exports parseFlac and parseVorbisCommentBlock

Verified by: tests/tree-shake.test.ts


Requirement: 100% statement, branch, function, and line coverage

The package SHALL achieve 100% Istanbul coverage on every .ts under src/ excluding .test.ts, .spec.ts, .types.ts, and barrels. Coverage gate failure SHALL block publish.

Scenario: coverage gate

  • WHEN bunx turbo test:coverage --filter=@suaveplan/audio-metadata runs
  • THEN statements / branches / functions / lines are each 100

Verified by: bunx turbo test:coverage --filter=@suaveplan/audio-metadata (CI gate)