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

@milehimikey/em-portal

v0.2.0

Published

Stakeholder-facing static portal for em event models — a multi-model, read-only browser built from `em export`/`em status --json`

Readme

em-portal

A stakeholder-facing, fully static portal for em event models — a read-only, multi-model browser built entirely from em export/em status JSON. No server, no database, no storage of its own: git stays the only history store, and em-portal build is a pure function of your .em files that a CI job runs and publishes to static hosting (GitHub Pages, an S3/GCS bucket, ...).

This executes the decision recorded in the em repo at docs/decisions/mil-162-teachable-navigator.md: a separate add-on tool, not a rework of em catalog, built for the audience em catalog names but doesn't fully serve — a non-technical stakeholder who wants "is this healthy" up front and a way to navigate a system of several models, not a flat per-model site.

Status

0.2.0 — read-only multi-model browser + guided first read. See What 0.1.0 includes and What 0.2.0 adds below.

Quickstart

npm install --save-dev @milehimikey/em @milehimikey/em-portal
npx em-portal build models/*/*.em -o site
site/
  index.html                 # landing page: em status rollup (state up front) + "First read" entry point
  <model-key>/
    index.html                # model page: diagram + slice table
    walkthrough.html          # guided first read (0.2.0): teaches the notation on THIS model
    diagram.svg
    slices/
      <slice-key>.html         # slice page: diagram, doc, driftSignal, PR link
      <slice-key>.svg

Open site/index.html directly (file:// works, no server needed) or serve the directory with anything that serves static files.

CLI

em-portal build <models...> [options]

Arguments:
  models             Paths to .em model files (same argument form as `em status`)

Options:
  -o, --out <dir>     Output directory (default: "site")
  --title <title>     Site title shown on the landing page (default: "em portal")
  --tests <dir>       Directory to scan for INV-* test citations (enables invariant
                       coverage on the landing page — forwarded to `em status --tests`)
  --repo <path>       Git repo to compute commits-behind-HEAD in (forwarded to
                       `em status --repo`; default: each model's own directory)

Multiple models in one invocation is how a multi-model system's cross-model index and em status rollup get built — same "one directory per model" convention examples/multi-model in the em repo documents:

em-portal build models/checkout/checkout.em models/fulfillment/fulfillment.em -o site

How it works

em-portal is a separate package from em — it never imports em's internal modules. Every fact it shows comes from shelling out to the installed em CLI (a real dependency of your project, resolved the normal npm way):

  • em export <file> — the normalized model as JSON (schema 1.8+): slices, elements, stable refs, the slice-doc frontmatter join (status, driftSignal, implementedIn, ...).
  • em status <files...> --json — the state-of-the-system rollup across every model given: slices by lifecycle status, drift-signal breakdown, invariant coverage (with --tests), open issues, and freshness (last-conformed revision, commits/slice-PRs behind HEAD).
  • em render <file> / em render <file> --slice <name> — the model's full diagram and each slice's own canonical-pattern diagram (SVG).

em export's doc join is deliberately frontmatter-only ("never the markdown body" — see the em repo's docs/cli.md); em-portal reads the bound slices/<key>.md file itself (the same doc.path the join already names) and renders its body with marked, the same markdown engine em catalog uses.

Consumers must tolerate unknown fields. em's own versioning policy is additive-minor-bump, breaking-major-bump; src/em/exportDoc.ts/statusDoc.ts declare only the fields em-portal reads (nothing is .strict()-validated), and checkExportSchemaCompatible/ checkStatusSchemaCompatible only warn on a major version mismatch. MIL-171 (landing in the em repo in parallel with this ticket) adds owner:/tracking: fields to the doc join — this is exactly the kind of change em-portal's ingestion layer is built to shrug off.

Deep links (MIL-173)

Portal URLs are built on em's own stable element refs (<sliceKey>/<kind>.<slug>, assigned by em export and edit-stable — inserting or reordering slices never changes an existing ref). One addressing scheme for both audiences: an agent citing checkout/event.order-placed (via em query/MCP) and a stakeholder clicking the matching row on a slice page mean the same element.

A slice page's URL already is its slice's own key (<model-key>/slices/<sliceKey>.html); every element row on that page carries its full ref as both its DOM id and a visible, copyable #-fragment permalink next to it (src/refs.ts). The full deep link to one element is:

<model-key>/slices/<sliceKey>.html#<sliceKey>/<kind>.<slug>

For example, in examples/order-fulfillment's Checkout slice, the Submit Payment command's deep link is:

order-fulfillment/slices/checkout.html#checkout/command.submit-payment

src/refs.ts exports elementDeepLink(modelKey, ref) / sliceUrl(modelKey, sliceKey) to build these, and parseDeepLink(link) to resolve one back into { modelKey, sliceKey, elementRef } (returns null for anything not shaped like a page this portal generates — an external URL, the site's own landing page, ...). The landing page's cross-model links table (below) uses the same builder to link straight at the specific publishing/referencing element on each side, not just the two models' index pages. Refs never depend on layout/coordinates or on the portal's own directory-naming choices for the <model-key> segment (see src/slug.ts) — only on em export's own ref-stability guarantee.

Multi-model navigation

The landing page indexes every model given to em-portal build and rolls up em status across all of them. Where one model's public event feeds another model's slice, em-portal resolves that as a cross-model link (src/crossModel.ts) — matching a public event's exact name against every other model's element names. This is a naming-convention join, not a compiler-verified reference: em has no DSL-level construct for "this element's trigger is another model file's public event" (confirmed against em validate's view-from-unresolved error — see the decision doc). If a link looks wrong, the fix is in the .em naming, not in em-portal.

What 0.1.0 includes

  • Package scaffold, CLI (em-portal build), export/status-ingestion tolerant of schema minor bumps.
  • Landing page: the em status rollup up front (slice counts, drift signal, invariant coverage, open issues, freshness/commits-behind-HEAD per model) — a reader's first question ("is this healthy") answered before any slice list.
  • Multi-model index + the cross-model link heuristic.
  • Per-model pages (diagram + slice table) and per-slice pages (diagram, rendered doc, driftSignal, implemented-in link).
  • Deep links on em's stable element refs (MIL-173) — see Deep links above.
  • Fully static, self-contained output; deterministic builds (same inputs -> byte-identical site, verified in test/build.test.ts); no LLM anywhere.

Deferred to 0.3.0 (MIL-175, "async review intake"): a "raise a question" affordance on a slice page, triaged into an issue "..." marker through the normal ratified path. The portal is still read-only otherwise.

Also deferred / open, not yet built:

  • A real GitHub Pages/bucket deploy workflow wired to a specific target repo — see CI publish recipe below for the pattern; this repo's own CI only builds and tests, since it has no real event models of its own to publish.
  • Parallelizing the per-model em export/em render calls (they run sequentially today — see Open questions).
  • Any client-side JS beyond the walkthrough's own stepper (search, filtering, sort across the slice table) — every other page is still plain static HTML, same posture as em catalog.

What 0.2.0 adds: the guided first read

MIL-174. A first-time, non-technical reader clicks "First read" — prominent on the landing page (pointed at the first model given to em-portal build) and on every model page (pointed at that model) — and gets a self-paced, Next/Prev walkthrough that teaches the notation using the team's own model as every example, never a generic glossary:

  1. Time runs left to right — anchored to the model's own first slice.
  2. A real event, command, and view/read model from this model, each with its definition and a color-keyed callout naming the actual element.
  3. Personas & swimlanes — this model's own declared personas and contexts.
  4. The four slice patterns (State Change, State View, Automation, Translation), each taught on the first real slice of that pattern this model has — a pattern the model doesn't use yet is still explained, but says so plainly ("this model has no Translation slice yet") instead of pointing at nothing.
  5. The three markers (amber note = linked design doc, red issue = open question, teal divergence = accepted deviation), each on a real marked-up element when this model has one.
  6. Status & ratification — the draft → reviewed → ready-to-implement → implemented lifecycle, a real example of this model's own slice status, that a named human ratifies (ratifiedBy), and a link to the landing page's status rollup as the "is this healthy" view.
  7. Where to go next: the slice table, a real slice page, the status rollup.

Every step is picked deterministically at build time from the model's own em export JSON (first occurrence in model order — see src/walkthroughSteps.ts, unit-tested independently of any HTML) and embedded in the generated page as a small JSON blob; a vanilla-JS stepper (no dependency, inlined, no fetch of any other page) drives Next/Prev, the left/right arrow keys, a numbered step nav, and a visible "Exit walkthrough" link/Escape key at every step. The step count is always the same 14 regardless of what the model contains, so the progress indicator ("Step 4 of 14") never depends on how much of the model's own vocabulary happens to show up.

Highlight mechanism (verified against a real em render SVG, not assumed): em's renderer already tags every element's node group with data-slice="<index>" and embeds an <metadata id="em-slices"> block — the same mechanism em watch --serve's own Review mode storyboard uses to spotlight one slice at a time. The walkthrough inlines the model's own diagram.svg directly into the page (not <object>-embedded, which isn't reliably script-reachable from file://) and reuses that same data-slice attribute to dim every other slice while a step is active — a real, intentional, already-load-bearing integration point, not a new one invented for this feature.

There is deliberately no per-element spotlight inside the diagram: em's SVG node ids come from a model-wide, name-only slug (makeId() in the em repo), independent of em export's own stable <sliceKey>/<kind>.<slug> ref scheme and never published as a stable contract — relying on it would mean treating an internal Graphviz <title> as an API. Single-element steps degrade honestly instead: a color-keyed callout card (kind + name + a same-page link to the real element), styled to match the diagram's own kind/marker colors, so it reads as the same visual language rather than a second one.

CI publish recipe

A downstream project (one that actually has .em models) publishes the portal from CI like this — build, then hand the output directory to whichever static-hosting deploy action you use:

# .github/workflows/portal.yml
name: em-portal
on:
  push:
    branches: [main]
permissions:
  contents: read
  pages: write
  id-token: write
jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci
      - run: npx em-portal build models/*/*.em -o site
      - uses: actions/upload-pages-artifact@v3
        with: { path: site }
      - uses: actions/deploy-pages@v4

This repo's own .github/workflows/ci.yml runs npm test (which builds the portal against the fixture models under test/fixtures/) on every push — the build half of the recipe above, exercised continuously; the deploy half needs a real target repo with real models, which this package intentionally isn't.

Test fixtures

test/fixtures/ holds .em files (plus their slices/*.md docs) copied verbatim from the em repo's own examples/order-fulfillment and examples/multi-model, generated by running that em checkout's CLI (npx tsx src/cli.ts from the em repo) against those examples and confirming they still compile clean. test/fixtures/invalid/broken.em is hand-written to exercise the refuse-on-error path (an unresolvable view ... from). test/fixtures/cross-model/ is a small hand-written pair (producer/consumer) exercising the cross-model link heuristic end to end: a public event in one model whose exact name appears inside two of the other model's element names.

Development

npm install
npm run build      # tsc -> dist/
npm test           # vitest — packs and shells out to the real @milehimikey/em devDependency
npm run typecheck

@milehimikey/em is installed from a packed tarball checked into vendor/ (npm pack output from the em repo) rather than a registry version, since em-portal is developed alongside unreleased em changes; see package.json's devDependency and re-pack with a newer em checkout as needed (cd ../em && npm run build && npm pack && cp *.tgz ../em-portal/vendor/, then bump the vendor/*.tgz filename referenced in package.json and npm install). A production install of em-portal in a downstream project instead takes @milehimikey/em as a normal (published) dependency — see Quickstart.

License

MIT