Marvin Mügge

Letting the demo write the wishlist

A deliberately small library has a growth problem, and it’s the mirror image of the usual one. The rules that keep NumType honest (a minimum viable NumPy, not a clone; nothing ships without its shape-typed core done properly) are also the rules that make every new operation suspicious. Add by ambition and the four-hundred-op clone arrives one reasonable addition at a time. So the second release started from a different question: not what should the library have, but what can be proven missing.

Proof from outside

That question can’t be answered from inside the library. I built a small retrieval application on the published package, the ranking core of a RAG pipeline, installed from the npm registry the way a stranger would install it, in a folder deliberately outside the build. Sixteen documents, eight queries, embeddings hashed from character trigrams from scratch, and one matmul scoring every query against every document at once. The whole thing is deterministic end to end, and every retrieval result is asserted, so the demo doubles as a test that fails loudly.

The friction log

The deliverable was the friction log. Wherever the natural line, the one a NumPy hand reaches for, didn’t exist, I wrote down a triple: the intent, the workaround I actually used, and what it cost. Ranking similarity scores meant leaving the array world for hand-rolled sort code. Dividing by a scalar meant wrapping the number in a one-element array and leaning on broadcasting. For a square root I looped over the raw buffer and rebuilt the array around the result. The log also records what didn’t rub. The normalization chain, the broadcasting and the single big matmul all flowed, and that half matters, because a friction log without a no-friction section can’t calibrate anything.

Evidence beats expectation

I had predictions, written down before the demo: mean, concatenation, stacking, argmax. The log confirmed some and embarrassed others. Ranking was confirmed twice over, the clearest gap in the surface. Mean turned out to be granular. What’s structurally missing is scalar division; mean falls out of it nearly free. A square root I hadn’t predicted stalled the pipeline twice. And concatenation, confidently expected, never came up, so it got no place on the list. The rule behind the method is strict. An operation enters the wishlist when a real program demonstrably needed it, however reasonable every other candidate sounded.

// the workaround the friction log recorded, ranking by hand in plain JS
const ranked = Array.from(rowScores.data)
  .map((score, docIdx) => ({ score, docIdx }))
  .sort((a, b) => b.score - a.score);

// the operation it asked for
const top = rowScores.topk(2);
//    ^ hover: { values: NDArray<[2]>; indices: NDArray<[2]> }

Five slices, five catches

The wishlist came out five items long. Ranking, scalar overloads, square root, stacking rows into a matrix, and a scalar read, each as its own slice with a binding spec, an implementation, and an independent verifier attacking the result from a fresh context before it counted. One habit from the first release paid for itself five times. Before each build I ran a separate review over the spec alone, and every single one caught a real blocker before any code existed — five for five. The verifiers found two more bugs mid-series; both were fixed inside their slices. And the old law held throughout: never wrong, only incomplete. Ask for the top k with a literal k and the result shape is [k], computed by the checker; ask with a plain number and the claim honestly degrades instead of guessing.

Closing the loop

One more proof came with the release itself. Before converting anything, I ran the untouched demo, still written against the first release with all its workarounds, against the new version. It passed unchanged, which means the compatibility promise held on a real consumer before any changelog claimed it. Only then did I rewrite the demo onto its own wishlist. Every workaround gave way to the operation it had asked for, every pinned similarity score came out byte-identical, and each friction comment now says in place what resolved it, so the paper trail runs through the source itself. The application that asked for the operations runs on them.

Small, still

NumType is still small; the second release grew it by exactly five operations, and every one carries its evidence. That is the growth rule now. The library expands at the speed real programs demand, one friction log at a time. A slower rule than a roadmap, and a much harder one to argue with.

NumType is on npm; the demo, the friction log and the five operation specs are in the research notes on GitHub.