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

pin-derive

v0.11.4

Published

Bidirectional constraint engine over numbers, choices, and subsets — Rust core, wasm handle ABI, host-agnostic ESM binding

Readme

pin-derive

license status core bindings

A model that answers from any end you ask it. A cell holds partial information about one of three things: a number (an interval), a choice (which one member of a declared universe), or a subset (which members). A relation narrows every cell it touches from every other, in every direction — there are no inputs and no outputs. Pin whichever facts you happen to know and the rest narrow themselves. There are exactly four outcomes and they are never blurred: a value is still possible, the constraints contradict each other, every value is a checked point, or a work limit stopped the run. Explanations are structural, not searched: every bound carries the set of moves that produced it, so "which pins conflict" and "why is this option gone" are reads.

net.pin(price, point(50)).pin(margin, point(0.2));  // profit derives to 10
net.pin(profit, point(10)).pin(price, point(50));   // margin derives to 0.2 — same relation

Why

Every team has a haunted spreadsheet.

It started as someone's quick model — pricing, a budget, a boss fight — and over the years the formulas compounded: a lookup here, a nested IF there, one sheet quietly referencing another. There's no VBA in it. The formulas alone are Turing-complete now, and the business runs on top of them. You don't touch it, because the moment you change one cell you can't predict what reflows.

Here's what nobody says out loud about that spreadsheet: it isn't one model. It's the same model, written over and over, once for every direction anyone ever needed to ask it a question from.

A boss fight has three numbers that relate: HP, damage per second, and how long the fight lasts. One fact: this fight takes about thirty seconds. The spreadsheet can't hold that fact. It can only compute forward, so it grows secondsFromHPAndDamage. Then someone wants to tune by feel, and it grows hpFromSecondsAndDamage. Then someone needs the weapon math, and it grows damageFromSecondsAndHP — the same relationship, hand-derived backwards three times, kept in sync by hand forever. One was patched last month. One wasn't. They disagree by about fifteen percent.

That comes from one assumption so baked in nobody questions it: a variable holds a value. Pin a cell and it's a number. Leave it alone and it's a hole: either empty, or anything, with nothing in between. The only way to ask "what would the inputs have to be?" in a world like that is to write a new function that re-derives them. So you do. And then you do it again.

pin-derive starts from the opposite assumption: a cell holds partial information. The relationship is written once and the engine runs it in every direction. Three things fall out that no frozen function can do:

  • Any direction, one model. Pin the HP and the damage, and the fight length derives. Pin the fight length instead, and the HP derives. No hpFromSecondsAndDamage. The explosion doesn't get cleaned up — it never happens.
  • The unknown is a space, not a crash. Pin too little and a cell comes back as what's still possible — somewhere between 40 and 60, one of these two samplers, three to five of these fifty items — for a person, a balancing loop, or an LLM to choose inside. Under-determination is the answer being honest about what it does not yet know.
  • The impossible points at itself. Push a value out of range and the one cell that broke reads ⊥ and names the exact moves that broke it, from the justification it was already carrying. Everything else keeps its value.

The second thing it does that a spreadsheet cannot: the categorical half lives in the network too. "This model permits these three samplers." "This model takes 4 to 12 steps." Pin the steps to 25 and the model is eliminated — by the same mechanism, with the same explanation, as a number going out of range. A choice and a number constrain each other, and so does a subset: "three to five distinct items from this catalog, under this budget, none of these" is one cell and one relation, not a validator.

It's for systems where someone — a human, a balancing loop, an LLM — is in the loop and needs to see what is still possible, pick inside it, and watch the rest re-derive. Guided generation. Configuration that stays consistent when you flip a knob. A model grounded by real limits instead of hallucinating past them.

Not a spreadsheet, not an industrial solver. A small lattice core in Rust, with a relation catalog and a chooser roster around it, that you teach the relationships of your domain once.

What it does

Three kinds of value, one lattice. Every cell is an Interval (a number, ±∞ allowed), a Finite (one member of a universe; the value is the set of members still possible), or a Set (a subset; the value is must and may). Each has meet, top, bottom, and point. Status is read off the value — free, bounded, point, conflicting — never declared; provenance is the orthogonal axis, pinned, chosen, derived.

Relations narrow in every direction. The catalog is the single source: one entry per relation, generating the constructors in all three languages, the document schema, the sentences describe() and why() render, and the reference table. cargo run -p pin-derive-core --bin catalog -- reference prints it.

| kind | relations | |---|---| | numeric | equal, sum, linear, product, affine, quantize, lt, lte, within, min, max, clamp, lookup, curve, pow | | choice | table (the one categorical relation), rank; and lte/min/max over an ordered universe | | subset | select (count-aware weighted selection), count, sum_over, member_of, subset_of | | helpers, which lower to one table | permits, ranges, compat, all, any | | moves | restrict, exclude (choice); has_all, has_none, exactly (subset) |

The helpers exist because an author says "the model permits these samplers," not "here is a table." They are authored by name, lowered by the engine, and exported back in the form they were written in.

Parameters that depend on a choice. A relation's parameter is a constant, a cell, by(choiceCell, map) — one constant per member, fired per still-possible member, eliminating members whose result cannot meet the target — or bands([[upTo, value], …]), a constant that depends on the magnitude of the value being related. quantize(width, raw, by(model, { krea2: 16, flux: 64 })) is one relation, not an enumeration of every multiple.

Explanations are structural. Every bound carries the bitset of moves whose narrowings produced it. A meet that reaches ⊥ records the union of the two justifications, so conflicts() is a read, not a search. why(cell) gives the narrowing records with a generated sentence; eliminated(cell) gives each removed member with the relation and the moves that removed it. The recorded set is sufficient, not minimal.

Live networks. Compile once; sessions are cheap to create, clone and fork. Adding cells and relations extends a solved session in place, because they can only narrow. Anything that could widen — removing a relation, widening a domain, adding a table row — makes a new revision and replays the ledger. Retraction is never in place: unpin and preview replay inside the engine.

Identity and compare-and-swap. solveFingerprint() identifies the input state (structure plus ledger); snapshotFingerprint() identifies the solved state; decisionDelta diffs two. A batch commits under an acceptance predicate with the base fingerprint as expect, so a proposal built against a state somebody has since committed into is refused as a stale base rather than landing on a state it never saw.

Choosers finish the job. Rounding (quantized cells under a pinned sum, by largest remainder), NearestDefault, Lp, and Fill ship in the box behind one Chooser trait, and no chooser invents a point on an infeasible network — it returns a structured refusal. pin-derive contains no randomness: stochastic choice reaches it through the same trait, from outside.

Performance

The core is built for interactive loops, not offline theorem proving. The scheduler is a worklist: a refinement that changes a cell queues the relations adjacent to it, each at most once until it fires, and a fire that changes nothing queues nothing. Ids resolve to indices at build time and the solve loop allocates nothing.

The numbers below are one release-build run of rust/core/examples/measurements.rs on one developer machine, reported as a guide rather than a gate. Reproduce them with cd rust && cargo run --release -p pin-derive-core --example measurements.

| workload | n | fires | refines | wall | |---|---:|---:|---:|---| | equal chain, build + solve | 300 | 598 | 1,197 | 0.175 ms solve / 0.326 ms build | | equal chain, build + solve | 1,000 | 1,998 | 3,997 | 0.574 ms solve / 0.419 ms build | | equal chain, build + solve | 3,000 | 5,998 | 11,997 | 1.644 ms solve / 1.110 ms build | | equal chain, build + solve | 10,000 | 19,998 | 39,997 | 5.128 ms solve / 3.640 ms build | | build + solve + drop networks of 20–50 cells | 10,000 | — | — | 22.7 µs each | | solves of a 10-cell network (fork + pin + solve) | 10,000 | — | — | 3.1 µs each | | 1,000-cell network with one 2,500-row table | 1,000 | 1,999 | 3,999 | 0.492 ms solve / 0.299 ms build | | document import, 1,000-cell chain | 1,000 | — | — | 1.578 ms | | extend a solved 1,000-cell session by one cell | 1 | 2 | 4 | 11.2 µs | | …then 100 more cells and relations, same session | 100 | — | — | 65.6 µs | | build-and-free cycles, 20 cells each | 10,000 | — | — | 12.9 µs each, RSS +32 KB |

The shape is the claim: 2.00 fires per relation from 300 cells to 10,000, so a chain stays linear, and extension appends the delta instead of recompiling the structure.

Through the JavaScript binding the same workloads pay the wasm boundary once per builder call and nothing per solve. One run of pnpm bench (js/bench.mjs), same machine:

| workload | n | wall | |---|---:|---| | 1,000-cell equal chain, cells / relations / first pin + solve | 1,000 | 0.468 ms / 2.287 ms / 0.604 ms | | 10,000-cell equal chain, cells / relations / first pin + solve | 10,000 | 3.598 ms / 20.050 ms / 4.894 ms | | 4-cell network, build + solve + dispose | 5,000 | 10.5 µs each | | 200-cell chain, one extra pin, re-solve, unpin | 200 | 126 µs each |

A relation crosses the boundary as JSON text, so declaring one costs about 2.3 µs through the binding against 1.2 µs for the same descriptor through the native document path; solving does not cross at all. On the same workloads the 0.10.2 TypeScript engine took 23 ms for the 1,000-cell chain, did not converge the 10,000-cell chain within its fire cap, and re-solved the 200-cell chain in 0.85 ms per pin.

Those are chains. On consumer-shaped networks the cost is measured separately in docs/specs/evidence/2026-09-05-scaling.md, which grows five shapes taken from the consumer tests until they stop. On 0.11.1 a 120,008-cell fan-out economy — eight global knobs over 10,000 entities — builds and solves in 93 ms and 107 MB natively, and the same network through the wasm binding solves in 1.1 s at a 278 MB peak of linear memory. A single sum over 100,000 bounded parts solves in 21 ms. The evidence file also names where each shape stops and why.

Getting started

The same program, one init line apart, in each language.

JavaScript

npm install pin-derive     # or: pnpm add pin-derive

Node 20+. The package is ESM with type declarations, and ships the wasm binary it loads. In Node, init() finds the bundled asset; in a browser, pass the bytes you fetched (wasmUrl is exported for that).

import { Network, init, point, product } from "pin-derive";

await init();

const net = new Network();
const price = net.cell("price", { lo: 0 });
const margin = net.cell("margin", { lo: 0 });
const profit = net.cell("profit", { lo: 0 });
net.relate(product(profit, price, margin));

// forward: pin price and margin, profit derives
net.pin(price, point(50)).pin(margin, point(0.2));
console.log(net.solve(), net.value(profit), net.status(profit));
// → converged { lo: 10, hi: 10 } point

The same network runs backwards — pin profit and price instead, and margin derives to { lo: 0.2, hi: 0.2 }. A Network owns a wasm handle: use using net = new Network(), or call net.dispose().

Rust

cargo add pin-derive-core
use pin_derive_core::network::{point, Network};
use pin_derive_core::relations::numeric::basic::product;
use pin_derive_core::session::SolveOutcome;

let mut net = Network::new();
let price = net.cell("price", None).unwrap();
let margin = net.cell("margin", None).unwrap();
let profit = net.cell("profit", None).unwrap();
net.relate(Box::new(product(profit, price, margin))).unwrap();

net.pin(price, point(50.0)).pin(margin, point(0.2));
assert_eq!(net.solve(), SolveOutcome::Converged);
println!("profit = {:?}", net.value(profit).as_interval().unwrap());
// → profit = Interval { lo: 10.0, hi: 10.0 }

The crate is the engine; the wasm module and both other bindings are built from it. See the Rust workspace README.

Python

uv add pin-derive        # or: pip install pin-derive

The wheel bundles the same wasm binary and loads it through wasmtime, so installing it does not require a Rust toolchain.

from pin_derive import Network, point, product

with Network() as net:
    price = net.cell("price", {"lo": 0})
    margin = net.cell("margin", {"lo": 0})
    profit = net.cell("profit", {"lo": 0})
    net.relate(product(profit, price, margin))

    net.pin(price, point(50)).pin(margin, point(0.2))
    print(net.solve(), net.value(profit), net.status(profit))
    # → converged {'lo': 10.0, 'hi': 10.0} point

See the package README.

Documents

A network is also a plain JSON document — cells, relations, and the pin/commit ledger — and import calls the same builder in document order, so there is no second construction path. Helpers appear by name and export writes back whichever form was authored. meta is opaque provenance the caller owns; it round-trips through decisions(), surface(), and export, so extract-build-solve-write-back is one loop.

{
  "cells": [
    { "id": "steps", "init": { "lo": 1, "hi": 100 } },
    { "id": "model", "kind": "finite", "universe": ["krea2", "flux"] }
  ],
  "relations": [
    { "type": "ranges", "a": "model",
      "map": { "krea2": { "steps": [4, 12] }, "flux": { "steps": [20, 30] } } }
  ],
  "pins": [{ "cell": "steps", "value": { "lo": 20, "hi": 30 } }]
}

kind defaults to "interval", so every 0.x document is a valid 0.11 document and the 0.x fixture corpus imports as-is. YAML is a documented stub. The document is what fingerprints are taken over.

Stability

Pre-1.0, so a behaviour change is a minor bump.

0.11 is a rewrite, not an upgrade. The engine, the builder API, the outcomes, and the explanation surface all changed. The 0.10 line is preserved by its tags (v0.10.0 … v0.10.2) and by the packages already published; nothing on those tags moves, and a ^0.10 range does not pull 0.11 in. The release notes carry the mapping from every 0.10 entry point to its 0.11 replacement.

The npm package pin-derive is now the wasm binding; pin-derive-wasm on npm is deprecated in favour of it. The crate pin-derive-core and the PyPI package pin-derive are both a new API. The current release on all three registries is 0.11.1.

Project layout

rust/core       crate pin-derive-core — the engine: values, catalog, relations,
                sessions, choosers, documents, reads
rust/wasm       the wasm module (handle ABI, ABI 4), built from the core
rust/python     PyPI pin-derive — the Python binding, bundling the wasm
js/             npm pin-derive — the JS binding: pin-derive.mjs, its .d.mts,
                the wasm asset, and the smoke suite
fixtures/       the 0.x document corpus, kept as a compatibility check
scripts/        wasm-build.mjs (the only wasm build) and the size gate
site/           the interactive docs site
docs/           REFERENCE.md (normative), DESIGN.md, EXTENDING.md,
                RELEASE-NOTES.md, specs/ (dated), archive/ (superseded)

Docs

  • API reference: the normative contract — the relation catalog, the builder, sessions and revisions, the four outcomes, justifications, identity, choosers, documents, and the wasm ABI
  • Design notes: what the engine is and where the design record lives
  • Extending guide: how to add a relation or a helper through the catalog
  • Release notes: what changed and what to change
  • Specs: dated specs, including the pin-derive 2 specification
  • Contributing guide: dev setup and the gates

The interactive docs site still documents 0.10 and is pinned to the published 0.10 packages. It is being ported to 0.11 separately from this repository's release line.

License

Apache-2.0.