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

@nbt-dev/seed

v0.1.0

Published

Deterministic synthetic-data generation for NBT cartridges: seeded randomness, distributions, fixed-offset calendars, funnel walks and a row planner.

Readme

@nbt-dev/seed

Deterministic synthetic data for NBT cartridges. Same seed, same rows, every time.

npm install @nbt-dev/seed

Read this before you import it

Inside a cartridge, use a namespace import.

import * as seed from "@nbt-dev/seed";        // correct
import { rng } from "@nbt-dev/seed";          // SILENTLY WRONG

A cart's action, workflow and view sources are rewritten before they reach esbuild: every import { X } from "src" becomes const X = __nbtEntity("src:X"), because that is how a cart names an entity in another cart. There is no exception for a package specifier and no error — the named form compiles green and hands you undefined at run time. The namespace and default forms are left alone, and esbuild resolves and tree-shakes them normally.

Ordinary Node, Deno, a *.seed.ts, a build script: either form is fine.

What it is

An engine, not a dataset. It holds no business numbers, no taxonomies and no domain knowledge — rates, mixes and stage names are yours, passed in as data. Keep them in the harness that owns them: this package publishes public, so anything in it is on the registry regardless of who bought what.

Four entry points, so a caller who wants a lognormal is not carrying a name pool:

| import | what | |---|---| | @nbt-dev/seed | seeded randomness, substreams, common random numbers, distributions, ids | | @nbt-dev/seed/time | fixed-offset calendars, booking slots, lead times, settle lags, seasonality | | @nbt-dev/seed/people | synthetic names, addresses, emails, phone numbers | | @nbt-dev/seed/funnel | stage walks, a defect layer, and a row planner |

A whole dataset from one number

import * as seed from "@nbt-dev/seed";
import * as time from "@nbt-dev/seed/time";
import * as people from "@nbt-dev/seed/people";
import * as funnel from "@nbt-dev/seed/funnel";

const FUNNEL = {
  start: "new",
  edges: {
    new:    [{ to: "booked", p: 0.28, lag: { medianDays: 1, p90Days: 9 } }],
    booked: [{ to: "showed", p: 0.66, lag: { medianDays: 3, p90Days: 14 } },
             { to: "noshow", p: 0.15, lag: { medianDays: 3, p90Days: 14 } }],
    showed: [{ to: "sold",   p: 0.088, lag: { medianDays: 5.2, p90Days: 66.6 } }],
  },
};

export function build(rootSeed: number, count: number, nowMs: number) {
  const s = seed.streams(rootSeed);
  const clock = time.clock(nowMs, -300);          // one location, one offset
  const plan = funnel.plan();

  for (let i = 0; i < count; i++) {
    const who = people.contact(s.of("contact", i), "northwind.example");
    const ref = plan.add("crm:Contact", { name: who.name, email: who.email, phone: who.phone });

    const steps = funnel.walk(s.of("funnel", i), FUNNEL, time.localMidnight(clock, -120));
    if (!funnel.reached(steps, "booked")) continue;

    const day = time.pickDayOffset(s.of("day", i), clock, {
      from: -30, to: 10, dowWeights: [0, 1, 1, 1, 1, 1, 0.4],   // no Sundays
    });
    plan.add("crm:Appointment", {
      contact: ref,                                // resolved to the created id
      startsAt: time.slot(s.of("slot", i), clock, day, { hourWeights: [[9, 64], [10, 75], [14, 18]] }),
      outcome: funnel.finalStage(steps),
    });
  }
  return plan;
}

plan.count() is a dry run — rows per entity, nothing written. plan.apply(writer) writes them in order and resolves the refs; writer is anything with create(entity, row) => Promise<{id}>, which SeedCtx from @nbt-dev/harness/seed already is.

For a large dataset, page it and carry the refs forward, or paging breaks every reference across the seam:

let refs = {};
for (let from = 0; from < plan.length; from += 500)
  refs = await plan.apply(writer, { from, count: 500, refs });

Four things it refuses to let you get wrong

Substreams, not one cursor. streams(seed).of("contact", 42) is independent of everything else. With a single shared generator, inserting one row upstream rewrites the whole dataset and a resumed run produces different data than an uninterrupted one.

A fixed offset, never a timezone. time takes offsetMinutes and does UTC arithmetic under it. There is no Intl in QuickJS, so toLocaleString and localeCompare are absent or silently different inside a console. And the boundary is where this actually bites: on a real appointment corpus, 120 of 8,685 rows fall on a different weekday in UTC than in the location's own time — enough to reorder a day-of-week ranking and give a confident answer to the wrong question.

Median and p90, never a mean. lognormalFrom(5.2, 66.6) and time.lag(...) take the two numbers people actually measure. An exponential forces p90/p50 = 3.32; a real settle time runs nearer 13, and fitting the exponential throws away the tail, which is the half anyone cares about. The signature will not accept a rate.

Defects on purpose. funnel.defect(r, 0.7, damage) exists because data that reads as real has the flaws real data has — a spelling that changed the year the form did, a backfill collapsing most of a date column onto one day, two ledgers of the same fact that disagree. A generator emitting only clean rows produces a demo where every aggregate agrees, which is the one thing production never does.

Constraints on this package's own source

It gets bundled into cartridges and scanned textually there, and the scanner cannot tell library code from author code. So none of these may appear in dist/:

Date.now · Math.random · crypto.randomUUID · db[ · email[ · vector[ · Intl. · localeCompare · toLocaleString · node: · require(

The build refuses on any of them and npm test asserts it. Nothing here reads a clock or an entropy source: ulidLike takes its epoch as an argument for exactly that reason.

Develop

npm install
npm run build     # esbuild per module + tsc types + the forbidden-token scan
npm test          # node --test

Licence CPAL-1.0.