25 KiB
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 inpackages/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.jsondependenciesis inspected - THEN no key starts with
@suaveplan/
Scenario: built dist references no platform globals
- WHEN
dist/is grep'd forwindow,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
ID3at byte 0 andTAGat length-128 - THEN
detectAudioFormatreturns"id3v2"
Scenario: WAV requires both RIFF and WAVE
- WHEN the buffer starts with
RIFFbut byte 8 onwards is notWAVE - THEN
detectAudioFormatdoes not return"wav"
Scenario: AIFC accepted as AIFF variant
- WHEN a buffer carries
FORMat byte 0 andAIFCat byte 8 - THEN
detectAudioFormatreturns"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
parseId3v2reads a TRCK frame value"3/12" - THEN
track.trackNumber === 3andtrack.totalTracks === 12
Scenario: MP4 trkn 6-byte structure maps correctly
- WHEN
parseMp4reads a trkn atom whose data is[0, 0, 0, 5, 0, 10](2 padding + 2 number + 2 total, big-endian) - THEN
track.trackNumber === 5andtrack.totalTracks === 10
Scenario: Vorbis multi-value tags preserved as array
- WHEN
parseVorbisCommentBlockreads twoARTIST=entries - THEN
track.artistequals the first value andtrack.rawTags.ARTISTis 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.trackNumberis byte 29 andtrack.commentis the 28-byte slice
Scenario: non-numeric year ignored
- WHEN the four year bytes contain non-digit characters
- THEN
track.yearisundefined
Scenario: empty result for missing magic or short buffer
- WHEN the buffer is shorter than 128 bytes or
TAGmagic 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.coverisundefinedeven if an APIC frame is present
Scenario: skipLyrics option suppresses USLT extraction
- WHEN caller passes
{ skipLyrics: true } - THEN
track.lyricsisundefinedeven 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
©dayatom 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
gnreatom encodes index 0 - THEN
track.genreisundefined(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=44100andtotalSamples=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_100andtrack.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
fmtchunk 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
datachunk but no precedingfmt - THEN
track.durationisundefined
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_100in 80-bit extended form - THEN
track.sampleRate === 44_100
Scenario: AIFC variant accepted
- WHEN byte 8 is
AIFCinstead ofAIFF - 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 }toextractMetadataor any individual parser - THEN
track.coverisundefinedregardless 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.lyricscontains the parsed text
Scenario: skipLyrics drops MP4 ©lyr
- WHEN caller passes
{ skipLyrics: true }to extractor against an MP4 file with©lyr - THEN
track.lyricsisundefined
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 toextractFromFile - 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"anderr.codematches 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
readUint32BEreads a byte sequence whose first byte ≥ 0x80 - THEN the result is unsigned (≥ 2_147_483_648)
Scenario: syncsafe int decoded
- WHEN
readSyncsafeIntreads[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
extractFromBufferruns in a Node environment without@opentelemetry/apiinstalled - 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
extractAudioMetadatareturns successfully against a FLAC buffer - THEN
result.format === "flac"andresult.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
parseFlacandparseVorbisCommentBlock
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-metadataruns - THEN statements / branches / functions / lines are each
100
Verified by: bunx turbo test:coverage --filter=@suaveplan/audio-metadata (CI gate)