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

@luraty/engine

v0.5.1

Published

Runtime-agnostic language-learning engine. No React, no React Native, no Expo, no database.

Readme

@luraty/engine

The runtime-agnostic core of Luraty — an adaptive language trainer for heritage speakers: people who grew up hearing a language, understand more than they can say, and are stuck between beginner courses that bore them and books that flatten them.

The engine is the backend. A mobile app is only a frontend. This package holds the decisions — what to teach, when, how an answer is judged, what happens next. It runs unchanged under Hermes (React Native), Node, and a browser, with zero framework imports.

Status: five slices shipped. Learner state and the evidence fold (record), persistence (serialize / deserialize), language packs (createPack / checkPack), coverage, and the scheduler (plan). The rules and the boundary are in CLAUDE.md.

Not built yet, and each is a missing input rather than a missing idea: interleaving by confusability (needs a relation the four-function pack contract cannot express), chunks as items (a unit key addresses a word), and fluency tasks (no timed-repetition model). See the notes on plan in src/core/plan.ts.

Thirty seconds

import { createProfile, day, learner } from '@luraty/engine';
import { de, vocabulary } from '@luraty/pack-de'; // already built — no createPack, no file reading

const d1 = day(1); // days start at 1 — `day(0)` is a compile error, 0 means "never" inside

// A learner who has met nothing. `variety` defaults to the pack's own id, `direction` to
// 'recognise', and `maxNew` below to half the session — all overridable, none invented.
let anna = learner(createProfile('de', d1), { pack: de, vocabulary });

// 1. PLACEMENT. However you asked, the answer is a list of claims — she says she knows these,
//    and nobody has checked. This is what stops day one being an empty screen.
anna = anna.claim(vocabulary.slice(0, 400), d1);

// 2. The engine decides. `maxNew` caps how much NEW material may crowd out review; it defaults to
//    half the session, which is where a year-long sweep puts the peak at every budget tested.
const session = anna.plan({ day: d1, maxItems: 12 });
//    Every item carries a `why`: 'verify' (she claimed it), 'new', 'relearn', or 'review'.
//    Show them differently — calling a word she grew up hearing "new" is the failure to avoid.

// 3. She answers. This is the only thing that can raise a word's rung.
anna = anna.answer('haus', 'known', d1);

// 4. How hard is this text for her now?
const hard = anna.coverage('Der Mann geht aus dem Haus.');
if (hard.kind === 'measured' && hard.band === 'in-band') {
  // just right: dense enough to learn from, sparse enough to read
}

// 5. Save the profile, and snapshot the numbers you want to show her later.
storage.set(anna.save());
history.append(anna.summary()); // known · claims confirmed / refuted / still unchecked

Every method above delegates to a free function that is still exported, and anna.profile is always reachable — the handle is a convenience, never a wall.

The full loop, with a comment on every line, is packs/de/src/example.test.ts. It is a test rather than a snippet so it cannot rot: it compiles against this engine and runs against the real 10,000-word German pack on every npm run check. This README once shipped a quickstart that had not compiled for three commits, which is why.

Want more in one day? maxItems is per CALL, not per day. Record what she did and call plan again — it dries up on its own, because answering sets lastAsked = today. Do not advance the day to unlock more: that tells the engine a night of sleep happened, which is what every interval in here is a claim about.

Watch it run for thirty days: npm run demo.

How fast is it on a phone? make bench — it runs the same bundle under Node and under Hermes, which is the runtime React Native actually ships, and projects the result onto an older device. make stress asks the other question: 250,000-unit profiles, megabyte texts, adversarial input, every scenario in its own process so a limit is named rather than fatal. Read docs/guides/benchmarking.md before quoting a number from it — in particular the device multiplier is an estimate until you calibrate it, and the guide says how.

Why it is a separate package

An adaptive engine is only testable if it is deterministic and runnable headless — you validate it by simulating thousands of learners, not by tapping through a UI. An engine that imports React Native cannot run in that harness at all.

The previous version of this engine lived inside the app. When the app was rewritten, the engine went with it — not because it was wrong, but because it was in the wrong place. This package exists so that can't happen twice.

The shape

plan(profile, { day, maxItems, maxNew }); //  → a session, plus a description of content it needs
record(profile, evidence); //                 → a new profile
coverage(profile, pack, query); //            → how much of this text they know
serialize(profile) / deserialize(text); //    → persistence, as pure total functions
createPack(config, data) / checkPack(pack, sample); // → a language, from JSON

Two of those signatures are shorter than you would expect, and both are findings rather than oversights. plan takes no pack — work the four-function contract through and nothing on it is reachable from a scheduling decision, so scheduling is language-free. It takes no seed either, because selection is by days-waiting and the key tiebreak is total, so there is no tie randomness could break. coverage takes raw text, not tokens, so nobody can hand it a token stream the pack never produced. The long-form arguments are in src/core/plan.ts and src/model/coverage.ts.

Everything is a value. No hidden state, no clock, no database. Time arrives as data (day) and content as a request the host fulfils. That is what makes a session replayable, a learner forkable, and two profiles diffable. Nothing in the engine draws a random number today; when something needs to, it takes a seed and derives from it, so call order can never change a decision.

Language packs

The engine knows no language. Everything language-specific is injected as a pack — four small functions plus data:

| | what it does | French | Arabic | German | | --------- | ------------------------ | ---------------- | --------------- | ----------------------------- | | split | text → pieces | on spaces | on spaces | on spaces | | key | are these the same word? | vaisaller | السوقسوق | Bahnhofstraßestrasse | | rank | how common is it | frequency list | frequency list | frequency list | | compare | is this answer right | fold accents | fold diacritics | umlauts two-letter (öoe) |

A pack is config plus data files, not code — so adding a language is two files rather than a pull request against the engine. Build one with createPack(config, data), then run checkPack(pack, sample) against real text: it catches the failures no unit test can, because fixtures are correct by construction and the input that breaks is the host's 20,000-word file. See docs/guides/adding-a-language.md.

One profile per language, deliberately: you are a different person in each one.

Develop

npm install
npm run check    # format + typecheck + lint + test — the whole gate

make on its own lists every command. Determinism is enforced by lint, not by convention: Date.now(), Math.random() and argless new Date() are errors in src/ — as are Intl, localeCompare() and normalize(), which work on Node and are missing or different under Hermes.

Use as a submodule

git submodule add https://github.com/younissk/luraty-engine.git engine

Then install it from npm with npm install @luraty/engine, or link a local checkout with "@luraty/engine": "file:../engine". Do not put the two in an npm workspace — hoisting would let react-native resolve from inside the engine and silently destroy the boundary this package is built around.