npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@miadi/ava8

v0.4.0

Published

The Ava8 symphony as a Miadi package — umbrella re-export of the ABC core, abcjs binding, React layer, atelier law and measurement, plus the ava8 CLI (render, tunebook, frames, midi, symphony, measure, verify, serve) and a standalone symphony server. ESM

Readme

@miadi/ava8

The Ava8 symphony, absorbed from jgwill/Ava8@3cf080a into four packages that can be taken separately, plus two later siblings that read back what the four write.

ESM only. Every one of them is "type": "module" with a single import condition and no CommonJS build. require('@miadi/ava8') fails with ERR_PACKAGE_PATH_NOT_EXPORTED; from a CommonJS file, reach it with await import('@miadi/ava8'). Node 18.17 or newer.

0.4.0

Two packages joined the family and the CLI grew the two verbs that read them. Additive: nothing from 0.3.x changed shape.

| Package | Holds | | --- | --- | | @miadi/ava8-atelier | the atelier's law as data — RegisterPlan, parseMode, the STRIDENCE thresholds and the verdicts they produce, the provenance block, the append-only consent ledger | | @miadi/ava8-measure | reading back — Standard MIDI decoding with register, mode, drum-position and note-for-note measures; spectral share and f0 over PCM; OSC movement analysis |

Both reach the root export as namespaces, atelier and measure:

import { atelier, measure, notesToMidiBytes } from '@miadi/ava8'

const bytes = await notesToMidiBytes(['C5', 'D5', 'E5', 'F5'], { tempo: 96 })
const verdict = measure.verify(bytes, { mode: 'cmajor', noteCount: 4 }, atelier.parseMode)

verdict.ok        // true
verdict.checked   // ['mode', 'noteCount'] — an empty list is NOT a pass
console.log(measure.formatVerdict(verdict))

That is not a second convention, it is a collision. @miadi/ava8-core and @miadi/ava8-atelier both export GM_DRUMS, DRUM_CHANNEL, BandsOverlap and WindowTooNarrow, and they disagree about the drum channel deliberately: core's is 10, the number a musician writes as %%MIDI channel 10, and the atelier's is 9, the nibble a parser reads off the wire. A name carried by two export * clauses is ambiguous, and an ambiguous star export is silently unreachable — so flattening these two in would have made "which channel are the drums on" a coin toss with no error message. Both packages are zero-dependency and DOM-free, so admitting them costs the root export nothing it was protecting: no node:*, no React, no jsdom joins the graph.

New verbs, documented under Reading back below:

ava8 measure render.mid                                  # what is actually in it
ava8 verify  render.mid --mode ddorian --kick 0,2,4,6    # exits non-zero when a claim fails

0.2.0

Four packages moved together. Two of the changes are breaking.

Breaking — notesToMidiBytes is async. It returns Promise<Uint8Array>.

-const bytes = notesToMidiBytes(glyph.notes, { tempo: 108 })
+const bytes = await notesToMidiBytes(glyph.notes, { tempo: 108 })

[email protected] is pure CommonJS. Importing it statically put a CommonJS module in @miadi/ava8-core's ESM graph, and the failure landed at module evaluation — so it took down every unrelated export of core, and of @miadi/ava8-abcjs through it, before a line of code ran. A no-bundler browser page could not import this package at all. The import now lives inside the one function that needs it, which is what makes that function async. There is no notesToMidiBytesSync.

Breaking — UseAbcPlayerResult.duration is now durationSeconds, named exactly as Ava8Player.durationSeconds one layer down.

Everything else is additive:

| Package | New in 0.2.0 | | --- | --- | | @miadi/ava8-core | abcBodyToNotes transcribes an ABC tune into note specs, so a melody is written once instead of twice. notesToMidiBytes takes chords, rests and per-note durations. abcKeyAccidentals, UnsupportedAbcError, NoteSpec. | | @miadi/ava8-abcjs | renderScoreToSvgString takes SvgExportOptions and serialises through XMLSerializer, so the markup carries xmlns and its own width/height. renderTunebook / renderTunebookToSvgStrings paint every tune. RenderResult carries tuneIndex/tuneCount. The player grew pause, resume, seek, setVolume, currentMs and a playback cursor. | | @miadi/ava8-react | pause/resume/seek/setVolume on the hook, a cursor on by default (highlightClassName), useAbcCursor, useAbcExport, scoreClassName/scoreStyle, renderError. | | @miadi/ava8 (this package) | ava8 tunebook, and the engraving flags --scale, --staff-width, --responsive, --padding, --print, --no-standalone, --tune-index, --selection-color — which parsed and were then discarded for the whole of 0.1.0. An unknown flag is now an error. The served player page gained a transport and a cursor. |

Two defects this release closes, both of which produced no error message at all:

  • ava8 render called the renderer with zero options. Every display flag parsed and vanished; the output was byte-identical whatever you passed. There was no CLI invocation that produced a correctly-sized SVG.
  • That output carried no xmlns and, in responsive mode, no width/height. Dropped into a host page it collapsed to 4px in the viewport corner, and neither a browser nor Inkscape would open the file as SVG.

Build status on this branch

All six packages are built and green.

| Package | State on this branch | | --- | --- | | @miadi/ava8-core | builtnode --test, including a resolver hook that makes midi-writer-js unreachable and imports the package anyway | | @miadi/ava8-abcjs | builtabcjs pinned exactly 6.6.4 | | @miadi/ava8-react | built — jsdom + react-dom/client | | @miadi/ava8-atelier | built — zero dependencies; a test asserts no band is exported as a default | | @miadi/ava8-measure | built — zero dependencies beyond the atelier; a hand-written FFT, so nothing is trusted that cannot be audited in one sitting | | @miadi/ava8 (this package) | built — every CLI verb as a spawned process, the server over real HTTP, the storage layer against a real filesystem, example/node/compose.mjs as a consumer, the vanilla example resolved against a planted hoisted node_modules as well as a nested one, ava8 frames read back off disk with the cursor required to cross the staff, and ava8 verify asserted to exit non-zero on a false claim and on no claim at all |

The frozen API surface each package was built to is rispecs/ABSORPTION-CONTRACT.md; what this package did with it, and where it went beyond it, is rispecs/ava8-umbrella.spec.md. Both ship in the tarball.

Three runnable consumers live in example/ — a node script that composes a cosmology which is not Chaosophia, a framework-free browser page, and a React component. example/README.md has the commands.

What the Ava8 symphony is

Ava8 (shipping as ChaoSophia) is a musical cosmology: a set of glyphs, each one carrying a name, an emoji, a colour, a note sequence and an ABC-notation tune. The prototype shipped four — spiral, puzzle, feather, crystal — with five instruments and per-glyph timbre overrides. A glyph can be rendered as a score, played through a soundfont, exported as MIDI, or sequenced with its siblings into a multi-movement symphony.

In the prototype that knowledge was fused to three things it does not need: Next.js (next/script), the shadcn/radix UI kit, and the four Ava8 glyphs themselves. The absorption follows that seam. The cosmology is now the default, not the designloadMusicData() replaces it with any other set of glyphs, so a consumer writing an unrelated composition can take the ABC rendering without inheriting a Next.js app or the Chaosophia glyphs.

The package map

The first four are the absorption. The last two arrived in 0.4.0 and go the other way: everything above them writes a file, and they read one back.

| Package | Runs where | Depends on | Take it when you want | | --- | --- | --- | --- | | @miadi/ava8-core | node + browser, no DOM, no React | midi-writer-js | ABC text model, ABC→notes transcription, notes↔MIDI, instruments, tempo, glyph cosmology, symphony composition — with no rendering and no audio | | @miadi/ava8-abcjs | browser + node (jsdom) | ava8-core, abcjs | score rendering, standalone SVG export and soundfont playback, driven from your own UI or from none | | @miadi/ava8-react | React 18/19 | ava8-core, ava8-abcjs, react (peer) | components and hooks that already own the render/play lifecycle — no radix, no tailwind | | @miadi/ava8-atelier | node + browser, nothing at all | nothing | the law as data: named non-overlapping register bands one of which must stay empty, mode parsing and purity, the spectral thresholds, provenance, a consent ledger | | @miadi/ava8-measure | node + browser, bytes in, never a path | ava8-atelier | to read a rendered artefact back — MIDI notes, registers, mode purity, drum positions, spectral share, f0, movement captures — and hold it to a claim | | @miadi/ava8 | node + browser | all five | one import for everything, plus the ava8 CLI and the standalone symphony server |

Rules the split enforces:

  • ava8-core touches no window and no document. A test walks the built output and fails if a DOM global appears in it.
  • ava8-abcjs returns bytes and DOM nodes; it never returns React.
  • ava8-react declares react/react-dom as peerDependencies, so a node-only consumer of @miadi/ava8 never installs React. React lives behind the ./react subpath export.

Install

# everything, including the CLI and the server
npm i @miadi/ava8
# or: pnpm add @miadi/ava8   /   yarn add @miadi/ava8

# or take only the layer you need
npm i @miadi/ava8-core                  # headless music model
npm i @miadi/ava8-abcjs                 # + rendering and playback
npm i @miadi/ava8-react react react-dom # + components and hooks
npm i @miadi/ava8-atelier               # the law: bands, modes, thresholds, consent
npm i @miadi/ava8-measure               # reading a rendered artefact back

Subpath exports of @miadi/ava8:

"."          -> re-export of ava8-core + ava8-abcjs
"./react"    -> re-export of ava8-react   (keeps React out of node consumers)
"./server"   -> createAva8Server
"./package.json"

Start here

1. Node script

Composes a symphony from the loaded cosmology and writes ABC and MIDI to disk. Every symbol here is exported from the built @miadi/ava8-core.

import { writeFile } from 'node:fs/promises'
import {
  buildSymphony, symphonyToAbc, symphonyDurationBeats,
  getGlyph, abcBodyToNotes, notesToMidiBytes, instrumentMidiProgram,
  parseAbcHeaders, validateAbc, tempoName,
} from '@miadi/ava8-core'

const symphony = buildSymphony({ title: 'Ava8', tempo: 108 })
const abc = symphonyToAbc(symphony)          // multi-tune ABC, X: renumbered 1..n

symphonyDurationBeats(symphony)              // 29
tempoName(108)                               // 'Moderato'
parseAbcHeaders(abc).title                   // 'Ava8 — The Spiral Awakens'
validateAbc(abc).valid                       // true

const spiral = getGlyph('spiral')
const bytes = await notesToMidiBytes(spiral.notes, {   // async since 0.2.0
  tempo: 108,
  program: instrumentMidiProgram('triangle'),  // 73, GM Flute
})

await writeFile('ava8.abc', abc)
await writeFile('spiral.mid', bytes)          // Uint8Array, never a Blob

A glyph carries its melody twice — as ABC text and as a notes array — and until 0.2.0 nothing checked the two agreed. abcBodyToNotes derives the second from the first, with the tune's real rhythm rather than one flat note length:

abcBodyToNotes(spiral.abcNotation)
// [ { pitch: 'C4', duration: '4' }, { pitch: 'Eb4', duration: '4' }, … ]

await notesToMidiBytes(abcBodyToNotes(spiral.abcNotation), { tempo: 108 })

The first thing it found was a disagreement in the shipped cosmology itself. spiral is K:Cmin and its notes array spells the thirds E4 — a natural, under a key signature carrying three flats. Read as notation the tune has an Eb4 there, and the two spellings have differed since the prototype. applyKeySignature: false transcribes letter-for-letter and reproduces the array as shipped; the default reads the notation as written. Neither is guessed at:

abcBodyToNotes(spiral.abcNotation, { applyKeySignature: false }).map((n) => n.pitch)
// ['C4', 'E4', 'G4', 'C5', 'G4', 'E4', 'C4']  — the notes array, exactly

Swapping the cosmology is one call — this is what makes the library usable for a composition that has nothing to do with Chaosophia:

import { loadMusicData, resetMusicData, glyphIds, instruments } from '@miadi/ava8-core'

loadMusicData({ glyphs: { /* your glyphs */ }, instruments: { /* yours */ }, glyphInstrumentSettings: {} })
glyphIds()        // your ids
instruments       // your registry — an ESM live binding, already updated

resetMusicData()  // back to spiral / puzzle / feather / crystal

2. Vanilla browser page

Plain ESM, no framework. Renders a score, plays it, follows it with a cursor.

<div id="score"></div>
<button id="play">Play</button>
<style>.ava8-playing, .ava8-playing * { fill: #5b4bdb; stroke: #5b4bdb; }</style>

<script type="module">
  import { getGlyph, renderScore, createPlayer, unlockAudio } from '@miadi/ava8'

  const abc = getGlyph('spiral').abcNotation
  const { visualObj } = await renderScore(document.getElementById('score'), abc, { responsive: true })

  const player = await createPlayer(abc, {
    instrumentId: 'triangle', tempo: 108,
    visualObj,                       // reuse the engraved tune — see below
    onEvent: (event) => { /* event.elements are the notes sounding right now */ },
  })

  document.getElementById('play').onclick = async () => {
    await unlockAudio()          // iOS needs a gesture before audio starts
    player.isPlaying ? player.stop() : await player.play()
  }
</script>

renderScore clears the container before drawing. createPlayer returns an Ava8Player with play(), stop(), pause(), resume(), seek(position, units), setVolume(0..1), destroy(), setTempo(bpm), and the readonly isPlaying, durationSeconds, currentMs and volume. stopAllAudio() is the process-wide teardown.

Handing visualObj back is what makes a cursor possible: without it createPlayer parses the ABC a second time into a throwaway target, and every timing event addresses real nodes that are not on the page — so highlighting them paints nothing.

loadAbcjs(source) / setDefaultAbcjsSource(source) choose how abcjs arrives — {strategy:'module'} (default), {strategy:'global'}, {strategy:'cdn', url?}, or {strategy:'inject', abcjs} for tests and SSR. Concurrent loadAbcjs() calls share one in-flight promise.

This page ships as example/vanilla/index.html + example/vanilla/app.js, with the cursor, a seek bar, a volume slider and a MIDI download. There is no bundler anywhere: it reaches the built package through an import map, and takes abcjs from the pinned CDN build.

Browser, without a bundler — what the import map needs

A bundler resolves all of this for you. A plain page resolves it itself, and the entries are not equally urgent:

<script type="importmap">
{
  "imports": {
    "@miadi/ava8":       "/node_modules/@miadi/ava8/dist/index.js",
    "@miadi/ava8-core":  "/node_modules/@miadi/ava8-core/dist/index.js",
    "@miadi/ava8-abcjs": "/node_modules/@miadi/ava8-abcjs/dist/index.js",
    "midi-writer-js":    "https://cdn.jsdelivr.net/npm/[email protected]/+esm"
  }
}
</script>
  • The umbrella and its two siblings are what @miadi/ava8 re-exports, so the browser resolves all three when the page imports the package by name. Engraving, playback, the cursor, transport and volume all work with exactly these three — abcjs is not mapped, because setDefaultAbcjsSource({strategy:'cdn'}) makes the package fetch its own pinned build.
  • midi-writer-js is needed by notesToMidiBytes and nothing else. Core reaches its CommonJS dependency through a dynamic import(), so an absent mapping surfaces the first time MIDI bytes are asked for — as MidiWriterUnavailableError, which names the specifier and quotes the line above. Leave it out and the score still engraves and plays; only the export fails. The CDN copy is verified against [email protected]; /node_modules/midi-writer-js/build/index.browser.js is the offline equivalent.

Where the sibling packages sit under node_modules is the installer's decision — npm and yarn hoist, pnpm nests — and an import map has one target per specifier and no fallbacks. The shipped example therefore lists its candidate paths, probes them with fetch, and writes the map before loading app.js, so the same two files work under either installer. Serve, over http, the directory that contains node_modules:

npx http-server . -p 8081
# http://127.0.0.1:8081/node_modules/@miadi/ava8/example/vanilla/index.html

If a package path cannot be reached the page says which specifier failed and every path it tried, and labels the CDN-only staff it draws instead as not @miadi/ava8.

3. React app

import { Ava8Provider, AbcPlayer, useAbcPlayer, useGlyphs } from '@miadi/ava8/react'
import { getGlyph } from '@miadi/ava8'

export default function App() {
  return (
    <Ava8Provider soundFontUrl="https://paulrosen.github.io/midi-js-soundfonts/FluidR3_GM/">
      <AbcPlayer abc={getGlyph('spiral').abcNotation} instrumentId="triangle" tempo={108} showScore />
    </Ava8Provider>
  )
}

AbcPlayer accepts a render-prop child receiving UseAbcPlayerResult, so the transport UI stays yours. Drop to the hooks when you want to draw every control yourself:

const {
  play, stop, toggle, pause, resume, seek, setVolume,
  isPlaying, isPaused, durationSeconds, currentMs, volume, error, ready,
} = useAbcPlayer(abc, { tempo: 108 })

const { glyph, glyphId, setGlyph, next, prev, ids } = useGlyphs('spiral')

The playback cursor is on by default: the sounding notes carry highlightClassName (default ava8-cursor), and since this package ships no CSS, one rule in your stylesheet is what turns it on. currentMs is a ref-backed getter rather than state — abcjs fires timing on every animation frame, and publishing that as state would re-render the host subtree sixty times a second.

AbcScore renders without playback; useAbcScore(ref, abc, opts) does the same against an element you own; useAbcExport(result) turns what is on screen into a standalone SVG string, a blob, or a print job. Component modules ship "use client", so Next.js App Router consumers import them unchanged. Every effect tears its player down on unmount and on abc change.

This component ships as example/react/App.tsx.

CLI

Bin name ava8, compiled to dist/bin.js.

| Verb | Does | | --- | --- | | ava8 glyphs | list the cosmology | | ava8 render <file.abc\|-> [-o out.svg] | headless standalone SVG via jsdom | | ava8 tunebook <file.abc\|-> -o <dir> | one SVG per tune of a multi-tune file | | ava8 frames <file.abc\|-> -o <dir> | one SVG per frame, cursor on the sounding note | | ava8 midi <file.abc\|-> [-o out.mid] | ABC → MIDI | | ava8 export <glyph> [-o out.mid] | glyph melody → MIDI | | ava8 symphony [--glyphs a,b,c] [--tempo N] [--title T] [-o out.abc] | compose a multi-movement score | | ava8 measure <file\|-> | read a rendered artefact — MIDI, WAV or a movement capture — and print what is in it | | ava8 verify <file\|-> [claims] | hold a rendered MIDI to claims, and exit non-zero when one fails | | ava8 serve [--port 8480] [--data <dir>] | standalone symphony server | | ava8 init [dir] [--force] | write music-data.json (chaosophia parity) | | ava8 info <file.abc\|-> | parsed headers + validation report | | --help / --version | |

A - file argument reads stdin; omitting -o writes stdout. Validation errors exit non-zero with the reason on stderr. An unknown flag is an error — nothing is accepted and then discarded.

ava8 glyphs
ava8 render tune.abc -o tune.svg
ava8 render tune.abc --no-responsive --scale 1.5 --staff-width 900 -o big.svg
ava8 tunebook symphony.abc -o ./score      # tune-1.svg, tune-2.svg, …
ava8 frames tune.abc -o ./frames --fps 30  # frame-001.svg, frame-002.svg, …
cat tune.abc | ava8 midi - > tune.mid
ava8 symphony --glyphs spiral,puzzle --tempo 120 --title Probe -o probe.abc
ava8 serve --port 8480 --data ./melodies

Engraving flags — render, tunebook and frames

| Flag | Does | | --- | --- | | --scale <n> | engraving scale. abcjs ignores it while responsive is on, so pass --no-responsive too | | --staff-width <n> | engraved staff width in points, default 740 | | --responsive / --no-responsive | size from a container instead of the staff width. Off by default for a file: a string has no container to be responsive to | | --padding <n> | engraving margin on all four sides | | --print | page layout rather than screen layout | | --no-standalone | omit the width/height a detached <svg> needs. Only for markup you are re-parenting into a container you styled yourself | | --tune-index <n> | which tune of a multi-tune file to engrave, 0-based | | --selection-color <css> | note highlight. abcjs hard-codes #ff0000 otherwise — the worst possible colour on a dark theme | | --all-tunes | on render, behave as tunebook. Needs -o <dir> |

ava8 render on a file holding more than one tune engraves one and says so on stderr. ava8 info reports the count under Tunes:. symphonyToAbc emits multi-tune ABC by design, so this is the common case, not the exotic one.

Frame sequences — ava8 frames

tunebook splits one ABC file across space: one tune, one file. frames splits the same file across time: one moment, one file, with the note sounding at that moment carrying a class you can style. The contract is the same one — -o <dir> is required, the directory is created, and the names are zero-padded to the digit width of the count, so frame-001.svg sorts and globs in order.

ava8 frames tune.abc -o ./frames                       # the whole tune, 30 fps
ava8 frames tune.abc -o ./frames --fps 12 --tempo 84   # slower reading, fewer files
ava8 frames tune.abc -o ./lead-in --from 0 --to 2000   # one window of it
ava8 frames tune.abc -o ./frames --cursor-class tide   # your class, your stylesheet

| Flag | Does | | --- | --- | | --fps <n> | frames per second, a whole number from 1 to 120. Default 30 | | --from <ms> | moment of the first frame. Default 0 | | --to <ms> | stop here instead of at the end of the tune. The window is [from, to) | | --cursor-class <c> | class written on the sounding note. Default ava8-cursor, the same class @miadi/ava8-react's cursor uses | | --tempo <bpm> | tempo the timeline is computed at. Omit it and the tune's own Q: decides |

The frame count comes from the tune: scoreTimings is asked where the score ends, and that is the same arithmetic the cursor lands on, so the last frame of a default run is the last moment the score has. Each frame's moment is computed from its own index rather than by adding an interval to the previous one — over 2700 frames an accumulated 1000 / fps drifts far enough to light the wrong note. A moment before the first note or past the last one is a frame with no cursor on it, not an error: a film's lead-in and tail are legitimate frames of the same score.

No CSS ships with the frames. One rule of your own dresses them, and dresses a live cursor in a browser at the same time:

.ava8-cursor { fill: #e26d5c; stroke: #e26d5c; }

frames writes SVG and stops there — exactly as render and tunebook do. There is no rasteriser, no encoder and no headless browser in this package. Turning a frame directory into a video is ffmpeg's work, and it belongs to whoever is making the film:

ava8 frames tune.abc -o ./frames --fps 30
# SVG is vector; rasterise first unless your ffmpeg was built with librsvg.
for f in ./frames/*.svg; do rsvg-convert -w 1920 "$f" -o "${f%.svg}.png"; done
ffmpeg -framerate 30 -i ./frames/frame-%03d.png -pix_fmt yuv420p tune.mp4

Keeping SVG as the master is the point of stopping here: the frames are re-encodable at any size, and a package that starts shelling out to encoders has stopped being a music package.

Reading back — ava8 measure and ava8 verify

Every verb above turns an intention into a file. These two go the other way. A generator's header states what it meant; at least three tools sit between that intention and the bytes on disk, and each of them has quietly changed something at least once. measure prints what a rendered artefact actually contains. verify holds it to claims and exits non-zero when one fails — that exit code is the entire product, because a verifier that always exits 0 is a decoration a build script will run forever without ever reporting anything.

ava8 measure render.mid            # notes, registers, tempo map, mode weights, drum grid
ava8 measure take.wav              # rate, length, spectral share in 2000-5000 Hz
ava8 measure movement.jsonl        # packets, held ratio, rates, onsets, heading, stillness
ava8 measure render.mid --json     # the same, as one JSON document

Dispatch is on content, never on the extension. MThd is Standard MIDI, RIFF/WAVE is a wave file, one JSON object a line carrying t and values is a movement capture. A .mid that is really a WAV is an ordinary accident of a pipeline with three tools in it, so the header wins and the disagreement is reported on stderr rather than becoming a parse error deep inside a reader.

ava8 verify render.mid \
  --empty 88-96 \
  --mode ddorian --min-purity 0.95 \
  --tempo 96,136 \
  --kick 0,2,4,6 \
  --seconds 8.5 --tolerance 0.2 \
  --notes 60 \
  --source previous.mid

| Claim | Holds when | | --- | --- | | --empty <LO-HI> | no melodic note falls inside that inclusive MIDI band. Channel 10 is excluded, so a drum kit never raises a false alarm | | --mode <name> | the sounding duration inside that mode is at least --min-purity. ddorian, "e phrygian", Bb-major, dsdorian for D sharp, or a bare list 2,4,6,7,9,11,1 | | --min-purity <s> | a share from 0 to 1. Default 1 — exactly pure | | --tempo <a,b,…> | the file's tempo events, in order, within --tempo-tolerance bpm (default 0.5) | | --kick <0,2,4,6> | note 36 lands on exactly those eighth-note slots within the bar, and nowhere else | | --seconds <s> --tolerance <s> | the last note ends there, give or take. Tolerance defaults to 0.5 s, which is tick arithmetic and not a judgement | | --source <f.mid> | the same material as another render, note-for-note; --multiset forgives the order and nothing else | | --notes <n> | there are exactly that many notes | | --json | the whole verdict as JSON on stdout, and nothing else on stdout |

Exit codes, which is what a script reads:

| Code | Means | | --- | --- | | 0 | every claim held, and there was at least one | | 1 | the artefact was read and a claim failed | | 2 | the command line was wrong, or no claim was made at all |

1 and 2 are deliberately different. "This file does not do what you said" and "you did not say anything" are opposite situations, and a pipeline that cannot tell them apart will report a green run for a check that never happened.

Two things that do not bend

The register band is never a default. --empty has no fallback value. A void band — a register left empty on purpose, so that a person can sing through it — is a measurement of that person's body. Shipping one as a default would publish it, and a published version cannot be unpublished after seventy-two hours. @miadi/ava8-atelier makes the same refusal one layer down by taking its bands as a required argument. So ava8 verify without --empty checks no band, and says so on stderr:

ava8: no register band was checked — pass --empty LO-HI to check one. This CLI
ships no default band, because a void band is a measurement of a living
person's voice and a default would publish it.

Silence there would read as a pass, which is the failure this sentence exists to prevent. A test in this package reads the compiled verb back and fails if a numeric band edge has grown into it.

Nothing checked is not everything passed. ava8 verify render.mid with no claim flags prints nothing was claimed, so nothing was checked — this is not a pass and exits 2. Verdict.ok from @miadi/ava8-measure is true in that case, and correctly so — no finding failed, because there were none — so the CLI requires that at least one claim was actually checked before it will report success. --json carries the same fact as checked: [] and nothingClaimed: true.

Two flags reach every verb: --json for machine-readable output where it makes sense (glyphs, info, symphony, measure, verify), and --music-data <file> — also honoured as MUSIC_DATA_PATH — to run the whole CLI against a different cosmology. The prototype set that variable for next start alone.

The prototype's exploratory bin/chaosophia.js verbs start/build/prepare/init are superseded. serve replaces start without a Next.js build in the consumer's working directory — that substitution is the point of the absorption.

Standalone server

createAva8Server(opts), exported from @miadi/ava8/server. Plain node http, no Next.js.

| Route | Response | | --- | --- | | GET /api/melodies | list, seeding the glyph cosmology on first call | | POST /api/melodies | {name, abc} save; 400 when either is missing | | GET /api/glyphs | the cosmology | | GET /api/health | {ok:true, store:'file'\|'memory'\|'upstash', glyphs:n} | | GET / | a self-contained player page consuming the bundle over ?abc= |

The player page has a full transport — play, pause/resume, stop, a seek bar and a volume slider — and follows the tune with a cursor. Its render parameters are abcjsRenderParams() serialised into the page rather than restated there, so the page, ava8 render and @miadi/ava8-react engrave the same ABC the same way.

Storage is an interface, not a vendor:

export interface Melody { name: string; abc: string; updatedAt?: string }
export interface MelodyStore {
  list(): Promise<Melody[]>
  get(name: string): Promise<Melody | null>
  save(melody: Melody): Promise<void>
  remove(name: string): Promise<void>
}
export function memoryStore(seed?: Melody[]): MelodyStore
export function fileStore(dir: string): MelodyStore
export function upstashStore(opts: { url: string; token: string }): MelodyStore

The server picks upstashStore when MELO_UPSTASH_REDIS_REST_URL and MELO_UPSTASH_REDIS_REST_TOKEN are both non-empty, otherwise fileStore when a data directory is given, otherwise memoryStore. The prototype's app/api/melodies/route.ts constructed its Redis client from those two env vars with an empty-string fallback and turned every resulting failure into HTTP 500, so the composer could not run without a Redis account. upstashStore is an optional dependency here; no store is required.

import { createAva8Server } from '@miadi/ava8/server'

const server = createAva8Server({ dataDir: './melodies' })
await server.listen(8480)
server.storeKind   // 'file'
server.url         // 'http://127.0.0.1:8480'  — loopback, not 0.0.0.0
await server.close()

fileStore writes one JSON file per melody and never lets a melody name become a path: the file name is a slug plus a truncated SHA-256 of the original name, so a melody called ../../secrets.txt lands inside the data directory like any other. upstashStore keeps the prototype's melody:<name> keys, refuses empty credentials at construction, and imports @upstash/redis lazily — absent, it raises MissingUpstashError naming the specifier instead of a blanket 500.

Absorbed from

Source prototype: jgwill/Ava8@3cf080a (branch main), plus the CLI exploration on origin/codex/explore-creating-distributable-package-with-cli-2025-06-21-22-18-57 (bin/chaosophia.js, SPECIFICATIONS.md). The prototype is read only — nothing in these packages writes back to it.

Rendering existed twice in the prototype and the two copies disagreed: components/abc-notation-renderer.tsx loaded abcjs from a jsdelivr <Script> pinned at 6.2.2, components/music-notation.tsx dynamic-imported the npm module (declared "abcjs": "latest"). The absorption reconciles them into one renderer with a pluggable loader strategy.

Four defects found in the prototype are closed on the way in: noteToMidiNumber throwing instead of returning NaN, the playback end-timer being cleared on stop, the iOS audio-unlock listeners no longer accumulating, and melody storage no longer answering a missing credential, a malformed melody and a network outage with the same HTTP 500.

Moving the jgwill/Ava8 app itself onto these packages, file by file: rispecs/MIGRATION-from-Ava8-app.md.

Documents

Every one of these ships inside the published tarball.

| Document | Holds | | --- | --- | | rispecs/ABSORPTION-CONTRACT.md | the four-package API surface and the testing bar | | rispecs/ava8-umbrella.spec.md | why this package exists: the distribution the prototype had no way to offer | | rispecs/MIGRATION-from-Ava8-app.md | prototype file → replacement import, one row per file | | example/README.md | how to run the three consumers |

The sibling READMEs are on npm: @miadi/ava8-core, @miadi/ava8-abcjs, @miadi/ava8-react, @miadi/ava8-atelier, @miadi/ava8-measure.

Build and test

npm run build   # tsc -p tsconfig.build.json, strict
npm test        # build, then node --test test/*.test.mjs

Every CLI verb is exercised as a real child process, with assertions on exit codes and on the bytes that reach stdout and disk. The server is driven over real HTTP. The storage layer runs against a real filesystem. example/node/compose.mjs is run as a consumer and its output files are parsed back.

License

MIT for the packaging. The prototype's own content is CC BY-NC 4.0, authored by Guillaume Descoteaux-Isabelle, with contributions from Gérico Tremblay.