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

@echelon-foundry/limen

v0.9.1

Published

Limen: an explicit boundary that keeps browser capabilities separate from application authority. A dependency-free browser kernel — HTTP, storage, clipboard and history as typed effects, with application state, transitions and decisions kept out of the br

Readme

Limen

An explicit boundary that keeps browser capabilities separate from application authority.

Limen (Latin): a threshold — the stone at the base of a doorway. Your application stands behind it. The browser stands in front of it. Nothing crosses except plain, serializable data.

npm install @echelon-foundry/limen

| | | | --- | --- | | Five-minute start | docs/quick-start.md | | Who owns what | docs/mental-model.md | | Where does my change go? | docs/where-code-goes.md | | I am an AI coding agent | AGENTS.md |


What it is

A dependency-free browser library that splits a web application in two:

  • The kernel runs in the browser and does browser things — DOM bindings, fetch, localStorage, the clipboard, history. It understands six HTML attributes and no application vocabulary at all.
  • The engine owns everything that means something — state, transitions, validation, what the user is allowed to do, what to show.

They communicate only through plain JSON-serializable data. The engine never touches a browser API; the kernel never learns what your data means.

The same npm package also ships a lifecycle CLI — init, status, verify, upgrade, doctor — which installs that boundary into a repository and keeps it honest.

Three things newcomers ask first

  1. Where is the WebAssembly? The npm package is still a TypeScript browser boundary and does not ship a WASM application engine. The repository now contains two real F#/.NET WebAssembly consumers of that boundary: the Limen product site under site/fsharp/, and the separate time-entry-state-machine application. The deployed Limen site therefore demonstrates the seam rather than merely describing it. Full status: docs/17-wasm-migration.md.
  2. Is Limen written in F#? The browser kernel and protocol are TypeScript. The lifecycle CLI is F#. The product site's application state, transitions, evidence handling, capabilities and projection are also F#, compiled to WebAssembly. A tiny C# [JSExport] file exists only as .NET marshalling glue; it contains no application decision.
  3. What happened to @echelon-foundry/typescript-wasm-kernel? It is the same package under its old name. From 0.7.0 Limen is published as @echelon-foundry/limen; the old name is deprecated, with 0.6.2 its last version. No exported symbol, file path, or protocol type was renamed — change the dependency name and the import specifiers, and run npx --yes @echelon-foundry/[email protected] upgrade if the lifecycle CLI is installed. See docs/18-naming-and-compatibility.md.

Why it exists

Most browser applications end up with the same problem: the truth about what the application is doing gets smeared across three places — a JavaScript store, the DOM itself, and the server. Each can disagree with the others, and no single file tells you which is right.

Limen takes a different position: exactly one place owns application state, and it is not the browser.

| Property | What Limen actually supplies | | --- | --- | | Explicit application authority | The contract gives the engine one place to publish application state and decisions. A compliant application treats the DOM as projection output, not as a second source of truth. | | Explicit effects and uncertainty | Browser I/O crosses as typed requests/results, including OutcomeUnknown where the browser cannot honestly call an effect success or failure. | | Portable engine seam | EngineTransport carries serializable data only. The site and an external time-entry consumer both demonstrate F#/.NET WASM engines behind it. | | Centralized browser authority | HTTP, storage, clipboard and navigation mechanisms live in the kernel rather than application transitions. | | Small browser-facing audit surface | The reference engine is mechanically checked for forbidden browser APIs; consumers can apply the same rule. | | Strong domain modelling is possible, not automatic | F# or a typed TypeScript engine can make illegal domain states unrepresentable and transitions pure. Limen does not manufacture that domain model for you. |

These are architectural properties or existence proofs, not measured claims about speed, defects, agent accuracy, tokens or cost — see Evidence. The mechanically enforced reference-engine boundary is: scripts/check-architecture.ts fails the build if src/engine/** so much as mentions document, window, fetch(, localStorage, or sessionStorage.

Core architecture

HTML / CSS / browser APIs
          │
          ▼
        Limen            ← the threshold: carries data, decides nothing
          │
          ▼
  Application engine
  state · transitions · decisions

In more detail:

flowchart LR
    subgraph browser["Browser — capability"]
        DOM["HTML + CSS<br/>data-* bindings"]
        K["Limen kernel<br/><i>src/kernel/</i>"]
    end
    subgraph app["Application — authority"]
        E["Engine<br/><i>src/engine/</i><br/>state · transitions · projection"]
    end

    DOM -- "DOM event" --> K
    K -- "SemanticEvent" --> E
    E -- "ViewState" --> K
    K -- "textContent, attributes, mount/unmount" --> DOM
    E -- "EffectRequest" --> K
    K -- "fetch / localStorage / clipboard / history" --> X(("Browser<br/>capabilities"))
    X -- "EffectResult" --> K
    K -- "EffectResult" --> E

One full interaction, step by step:

DOM event  →  Limen  →  SemanticEvent  →  transition()  →  new state
                                                            │
                                      effect needed? ───────┤
                                                            ▼
                                                    EffectRequest
                                                            │
                                                          Limen
                                                            │
                                                   browser capability
                                                            │
                                                       EffectResult
                                                            │
                                                       transition()
                                                            ▼
                                                     project() → ViewState
                                                            │
                                                          Limen
                                                            ▼
                                                        the DOM

The two arrows crossing the middle are the entire contract. They are defined in src/protocol.ts — about 160 lines, and the single most useful file to read.

Three complete interactions traced through every file they touch: docs/traces.md.

More than one application engine

A large application does not have to become one large WebAssembly binary. Limen now ships a language-neutral federation runtime for independently loaded bounded state systems:

BrowserKernel
     |
application shell
     |
ModuleFederation
  /    |     \
WASM A WASM B WASM C
 state  state  state

Each module owns its own state and legal transitions. Cross-module work uses versioned, JSON-safe envelopes; another module can request a transition but can never mutate the owner's state directly. Manifests declare accepted/emitted contract versions, dependencies, capabilities and routes. The federation layer checks those transport facts while remaining blind to application meaning. Transport exceptions fault only the affected module, can be observed through payload-safe diagnostics, and startAvailable() can continue unrelated modules while leaving dependents of unavailable modules blocked. Limen does not retry or invent recovery policy.

The runtime is implemented and tested, and the repository now includes a real multi-F#-WASM existence proof. The federation.html proof loads two independently published .NET WebAssembly runtimes, requires distinct runtime IDs, routes a versioned transition request/result through ModuleFederation, and snapshots each module independently. The main product application still uses one F# engine; the proof is deliberately separate so the site is not split without a real state-ownership reason.

Full design and API: docs/23-wasm-federation.md.

Install

npm install @echelon-foundry/limen

No runtime dependencies. Node ≥ 22 to build or test; any browser with ES2022 modules, fetch and AbortController to run.

The smallest working example

Three files. Nothing elided.

index.html — structure and bindings:

<button data-event="increment">Add one</button>
<button data-event="reset" data-bind-disabled="resetDisabled">Reset</button>
<p>Count: <span data-text="count">0</span></p>

<script type="module" src="./main.js"></script>

engine.ts — all the meaning:

import type { BrowserToEngineMessage, EngineToBrowserMessage, EngineTransport, ViewState }
  from "@echelon-foundry/limen/protocol";

type State = { readonly count: number };

// Pure. No DOM, no fetch, no globals.
const transition = (state: State, name: string): State => {
  switch (name) {
    case "increment": return { count: state.count + 1 };
    case "reset":     return { count: 0 };
    default: throw new Error(`Unrecognized event: ${name}`);
  }
};

// Pure. Produces what the view needs — including whether Reset is *allowed*.
const project = (state: State): ViewState => ({
  count: state.count,
  resetDisabled: state.count === 0,
});

export function createCounterTransport(): EngineTransport {
  let state: State = { count: 0 };
  return {
    async start() {},
    async dispatch(message: BrowserToEngineMessage): Promise<EngineToBrowserMessage> {
      if (message.kind === "Event") state = transition(state, message.event.name);
      return { view: project(state), effects: [], cancellations: [] };
    },
  };
}

main.ts — the wiring, in full:

import { BrowserKernel } from "@echelon-foundry/limen";
import { createCounterTransport } from "./engine.js";

await new BrowserKernel(createCounterTransport(), document).start();

A click becomes SemanticEvent { name: "increment" }, transition returns a new state, project turns it into { count: 1, resetDisabled: false }, and the kernel writes 1 into the <span> and enables the button.

Note resetDisabled. The engine decides whether Reset is available; the DOM never re-derives it from the number on screen. That habit — project capabilities, don't reconstruct them — is most of what using Limen well consists of.

This example is real, and the test suite executes it on every run: examples/01-counter/. A no-build-step JavaScript copy ships inside the npm package at examples/minimal/.

How state works

  • One place owns it: your engine. Not the DOM, not a store, not a module variable.
  • It is a discriminated union, so combinations that make no sense cannot be written down.
  • Transitions are pure: (state, command) → { state, effects, accepted }. An illegal command is refused explicitly and says so, rather than throwing or quietly working.
  • The view is a projection, project(state) → ViewState: plain named values and lists. It includes what the UI is allowed to do, so the DOM never works that out for itself.
  • Nothing persists by itself. A reload starts from your initial state unless your engine asked for a Storage effect.

Detail: docs/04-state-model.md.

How browser capabilities work

The engine never performs an effect. It describes one; the kernel performs it and reports a typed outcome, which the engine then treats as evidence.

| Capability | Operations | Outcomes | | --- | --- | --- | | Http | any method, your headers and body, a required timeout | Success · Failure · Cancelled · OutcomeUnknown | | Storage | get / set / remove on localStorage | Success · Failure | | Clipboard | writeText (read is deliberately absent) | Success · Failure{denied, unavailable, unknown} | | Navigation | push / replace / back / forward | Success{location} · Dispatched · Failure |

Clipboard and Navigation are new in 0.6.1. On an earlier version they do not exist, and requesting one produces a BridgeError with phase: "effect" and no result. Check with npm ls @echelon-foundry/limen; the per-version record is CHANGELOG.md.

Plus one message nobody requested: LocationChanged, when the user presses Back or Forward. It is not an effect result, because no effect was asked for.

OutcomeUnknown is the one people skip, and the reason the set is worth having: a timed-out request may already have reached the server. A POST that times out must not be retried automatically, and no type that collapses that into "failed" can tell you so.

Not implemented: files, timers, focus control, geolocation, IndexedDB, clipboard read. Adding one is a deliberate protocol change — docs/15-recipes.md.

Detail: effects and browser interop · routing · clipboard.

F# and this repository

Being precise, because this is the most common misunderstanding:

| Part | Language | Status | | --- | --- | --- | | Browser kernel + protocol (src/kernel/, src/protocol.ts) | TypeScript | shipped in the npm package | | Reference engine (src/engine/) | TypeScript | shipped for examples/reference use | | Lifecycle CLI (cli/Limen.Core/, cli/Limen.Cli/) | F# | shipped as self-contained platform binaries | | Product-site application engine (site/fsharp/Limen.Site.Engine/) | F# | compiled to .NET WebAssembly and used by this site's interactive pages | | Product-site WASM host | tiny C# marshalling shim | one [JSExport]; no state or application rule | | Product-site browser mechanics | TypeScript | BrowserKernel + a loader/JSON transport only |

The important distinction is not "TypeScript versus F#." It is mechanism versus authority. Limen's browser side remains generic while the application engine may be TypeScript, F#, or another language behind EngineTransport.

The product site is now an in-repository existence proof of that seam. Its F# engine owns release legality, evidence, stale-result rejection, reconciliation, the placement challenge, and projection. The browser-side site code owns none of those concepts.

See docs/17-wasm-migration.md for the exact implementation and remaining limits.

Examples

Each is executed by test/examples.test.ts against its own real index.html, so none can silently rot. Every one has a README covering its state model, event and effect flow, exercises, and the mistakes people actually make with it.

| Example | Demonstrates | | --- | --- | | 01-counter | the minimum: event → transition → projection → DOM | | 02-form | validation, capability projection, rejected illegal transitions | | 03-fetch-data | HTTP effect, lists, all four outcomes, stale-result rejection | | 04-save-data | full save lifecycle, storage effect, non-idempotent write safety | | 05-multi-screen | screens as state, shared vs. screen-local lifetimes, no URLs | | 06-time-entries | a realistic feature: load, validate, add, mutate, refresh | | 07-clipboard | copying text; three failure reasons, only one worth retrying | | 08-routing | typed routes, deep links, Back and Forward, static hosting | | minimal | the copy shipped inside the npm package — four files, no build step | | kitchen-sink | every primitive and every outcome, interactively |

Documentation

| You are… | Start here | | --- | --- | | Getting something working now | quick-start | | Trying to understand the model | mental model | | Deciding where a change belongs | where does code go? | | Following one interaction end to end | three traces | | Looking for an API | API reference | | Doing one specific task | recipes | | Debugging | troubleshooting | | Adding URLs | routing | | Copying text | clipboard | | About to do it wrong | anti-patterns | | Adopting Limen elsewhere | integration guide | | Using the CLI | lifecycle CLI | | Splitting an application across multiple WASMs | WASM federation |

Full index: docs/README.md. Live site: https://kemiller2002.github.io/limen/.

For agents

Start at AGENTS.md. It opens with the non-negotiable rules, then gives a deterministic reading order, repository landmarks with real paths, a placement decision tree, and a list of mistakes agents actually make here.

The three documents that answer most placement questions on their own: mental model, where does code go?, three traces.

Deeper guidance: agent guide.

The npm package

npm install @echelon-foundry/limen

What you receive:

| | | | --- | --- | | Entry points | . (kernel + types), ./protocol, ./kernel, ./reference-engine | | Types | .d.ts for everything, with source maps | | Executables | limen (and its legacy alias typescript-wasm-kernel) — the lifecycle CLI | | Documentation | README.md, CHANGELOG.md, LICENSE, and docs/: quick start, mental model, where-code-goes, API reference, troubleshooting, glossary | | A complete example | examples/minimal/ — four files, no build step | | Runtime dependencies | none |

The packaged docs describe the version you installed. The copies on GitHub describe main, which may be ahead.

The tarball's contents are verified on every run of npm run check against an expected manifest, and a clean-room job installs the packed tarball into an empty project and builds the minimal example from it — so "it works in the repository" is never mistaken for "it works when installed".

The lifecycle CLI

The same package is also a command-line tool that installs the Limen boundary into a repository and keeps it honest. No .NET runtime is required — a self-contained binary ships for each supported platform.

npx @echelon-foundry/limen init      # install the boundary. Idempotent.
npx @echelon-foundry/limen status    # what is installed, and is it valid?
npx @echelon-foundry/limen verify    # check it. Read-only.
npx @echelon-foundry/limen upgrade   # move to this version, safely.
npx @echelon-foundry/limen doctor    # explain what is wrong, and how to fix it.

For a .NET guest engine, npx @echelon-foundry/limen wasm-check <build-output> checks the built assemblies against the WebAssembly interpreter's per-method frame limit before the browser finds it (docs/57).

Once installed, the executable is simply limen.

init creates three things: limen.config.json (yours — it names which directories are engine and which are kernel), a CI workflow that runs verify --strict pinned to the version recorded in the installation manifest, and that manifest at .echelon/limen.json. It never overwrites a file you have edited, and never overwrites a file that was there before it arrived. Running it twice makes no second round of changes.

verify then enforces the boundary this README opens with, using the same rule set Limen applies to its own repository (architecture/boundary-rules.json): engine code must not acquire browser, JavaScript-interop, network, filesystem or process authority, and neither side may use eval. A boundary that checks no engine code is reported as not-configured (exit 8), never as a pass; a repository with no browser application declares "boundary": { "notApplicable": { "rationale": "…" } }. It is a lexical check — a guard rail, not a proof.

For CI and agents, every command takes --json and branches on stable exit codes; init and upgrade take --dry-run and --check. Nothing prompts, so nothing hangs.

Full reference: docs/20-lifecycle-cli.md. What it writes and what an upgrade may change: docs/21-installation-and-upgrade.md.

The CLI is implemented in F# (cli/Limen.Core/, cli/Limen.Cli/); the Node side is a launcher that selects a binary and forwards arguments, and contains no lifecycle logic.

Compatibility

  • Node ≥ 22 to build and test (the published package is browser code)
  • TypeScript ≥ 5.9 if you consume the types
  • Browsers: any with ES2022 modules, fetch, and AbortController. The clipboard capability additionally requires a secure context (https:// or localhost)
  • Runtime dependencies: none — the kernel imports nothing at runtime
  • The CLI: needs no .NET runtime; a self-contained binary ships for Linux x64/arm64, Windows x64, and macOS x64/arm64. Any other platform exits 7 saying so. Building it from source needs the .NET SDK 8 and F# 8.
  • WASM: not required by the npm kernel. This repository's product site does use .NET WebAssembly for its F# application engine. Consumers may use the in-process TypeScript reference engine or provide another EngineTransport. See docs/17-wasm-migration.md.
  • Versioning: semver, currently 0.x — the protocol may still change in a minor release. See stability and compatibility.

The site

Limen's own website is built with Limen — its interactive sections are a real Limen application driven by the same package you would install, and its prose is ordinary static HTML. Source in site/, deployed by .github/workflows/pages.yml.

npm run serve:site   # build and serve on http://localhost:4174

The release demo performs real Limen HTTP requests for success and network failure. Its timeout-after-dispatch control injects an already-classified OutcomeUnknown into the F# engine so the recovery path is deterministic on a static Pages site; the kernel's timeout classification itself is covered by kernel tests. The stale-evidence and placement challenges are deterministic F# state-machine scenarios rather than animations.

Development

npm install
npm run check              # the gate: build, checks, and all tests
npm run build              # tsc → dist/
npm run build:examples     # tsc → examples/**/*.js
npm run check:architecture # boundary enforcement
npm run check:docs         # links, paths, orphans
npm run check:package      # the npm tarball's contents and its links
npm run check:clean-room   # pack, install into an empty project, build the minimal example
npm run smoke:browser      # exercise the DOM behavior in a real Chromium (needs Playwright)

Then, to see it:

python3 -m http.server 4173

| | | | --- | --- | | Reference feature | http://localhost:4173/ | | Examples | /examples/01-counter/ … /examples/08-routing/ | | Every primitive, interactively | /examples/kitchen-sink.html |

The network-backed examples call endpoints that do not exist without a backend. That is deliberate — they demonstrate the typed failure states.

npm run smoke:browser drives the counter, clipboard and routing examples in a real Chromium — including reading the clipboard back and pressing the browser's own Back and Forward buttons, neither of which jsdom can settle. Playwright is not a dependency; install it yourself (npm i -g playwright && playwright install chromium) and the script runs, or skips with a message and exits 0 if it is absent.

Working on the lifecycle CLI additionally needs the .NET SDK 8:

npm run test:cli           # dotnet test — the F# lifecycle core
npm run build:cli          # publish the binary for this platform
npm run build:cli:all      # publish all five platform binaries (what npm pack ships)

npm run check runs without the .NET SDK; the CLI tests report as skipped rather than passing when no binary has been built.

Contributing — including AI agents — starts with AGENTS.md and docs/12-design-rules.md.

This repository follows SDE and the ROS work protocol: identify a work item and run ./ros work start WI-#### before meaningful changes, or CI's validate job will reject the branch.

Installed ROS governance references are also part of the repository's documented engineering surface: development telemetry and Ordo observation and handoff.

Evidence

Limen's benefits are stated above as architectural consequences — things that follow from the boundary — not as measured outcomes.

No controlled measurement of Limen's effect on development time, token consumption, cost, or defect rate has been performed. Where evidence exists, it is about SDE, or about language and style — not about Limen. Those are not conflated, and no quantitative claim about Limen appears anywhere in this documentation.

Full accounting, including what a Limen trial would have to measure and why one has not been run: docs/19-evidence.md.

Known gaps, deferred work, and the reasoning behind both are tracked in docs/ROADMAP.md and docs/DOCUMENTATION-AUDIT.md.

Tradeoffs

Real, and documented rather than hidden: no route table or path matching (the Navigation capability is the mechanism, not a router), no capabilities beyond the four above, no focus management, no list virtualization, no scheduling primitives, and more ceremony than a small component framework for a genuinely simple page. See docs/01-architecture.md § Honest limits.

Release

CI builds, tests, and validates the npm tarball on every push and pull request. Publishing runs in .github/workflows/publish.yml and authenticates with npm Trusted Publishing over GitHub OIDC — no npm token is stored in GitHub.

Get the version bump (package.json, lockfile, CHANGELOG) merged to main, then take either path:

  • Push the tag. git tag -a vX.Y.Z <sha> -m "Limen X.Y.Z" and git push origin vX.Y.Z.
  • Or run "Publish" from the Actions tab on main, with version (X.Y.Z, no v) and commit (the full 40-character SHA). Before anything is tagged, the run requires the commit to be on main, its package.json to carry that version, its CI run to have passed, and vX.Y.Z to be absent or already at that commit. It then pushes the annotated tag itself and publishes in the same run.

Either way the workflow rejects a tag whose version does not match package.json, rebuilds and runs all checks on the tagged commit, publishes the tested archive, and creates the GitHub release with the version's CHANGELOG section as notes. A re-run is safe: an existing matching tag, an already-published version and an existing release are each left alone. On the dispatch path, npm's provenance names the main commit the workflow ran from; the tag names the released commit.

F# packages. The same run packs EchelonFoundry.Limen.Contract, EchelonFoundry.Limen.Guest, EchelonFoundry.Limen.Store and EchelonFoundry.Limen.Routing at the npm version (lockstep), proves them in a clean room (npm run pack:nuget, npm run check:nuget), attests them with Sigstore build provenance, and attaches them, with checksums.txt, to the GitHub release. It then downloads them back and verifies each checksum and attestation. To check a release yourself, from an empty directory:

gh release download vX.Y.Z --repo kemiller2002/limen --pattern '*.nupkg' --pattern checksums.txt
sha256sum -c checksums.txt
for f in *.nupkg checksums.txt; do gh attestation verify "$f" --repo kemiller2002/limen; done

Consumers install them through Conditor's NuGet release-asset feed, never a public feed: declare the limen-fsharp component in conditor.json and run conditor upgrade --current (docs/41).

iPad Safari. CI runs the store pack in Playwright WebKit, which is not iOS storage policy. Each release's notes carry a manual iPad Safari checklist; run it on a device before announcing the release.

Changes by version: CHANGELOG.md.

License

Source available under PolyForm Noncommercial 1.0.0 for permitted noncommercial uses. Commercial use requires a separate license from Echelon Foundry. Previously distributed versions remain subject to the licenses under which they were released.

Commercial licensing information is forthcoming.