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

tag-hierarchy

v0.2.1

Published

A tiny, zero-dependency projection kernel: flat tagged items + a declarative query → an on-demand hierarchy tree. Pure, immutable, deterministic.

Readme

tag-hierarchy

A hierarchy is a lossy projection of a flat tagged relation.

A tiny, zero-dependency, framework-agnostic projection kernel. Hand it a flat set of { id, tags } items plus a declarative query and it returns a tree — projected on demand. Storage stays flat; structure, grouping, and ordering all fall out of one tag mechanism.

import { tagQuery } from 'tag-hierarchy';

const items = [
  { id: '1', tags: ['project:auth', 'status:open', 'prio:003'] },
  { id: '2', tags: ['project:auth', 'status:done', 'prio:001'] },
  { id: '3', tags: ['project:ui',   'status:open', 'prio:002'] },
];

// Group open items by project, ordered by priority.
const tree = tagQuery(
  { all: ['status:open'], else: 'keep', order: { by: 'tag', prefix: 'prio:' } },
  items,
);

The query grammar

A query is a plain object; every field is optional. It is declarative and order-free — an LLM (or a human) can author it and read it back to explain exactly what a view does.

{
  all?:   string[]          // AND-set: an item must carry ALL of these tags to match here
  then?:  Query[]           // sub-queries that refine the matched set (each claims its items)
  else?:  'drop' | 'keep'   // what happens to the remainder (default 'drop')
  order?: OrderSpec         // how to order the LEAVES at this level
  rest?:  boolean           // deprecated alias of `else` (true = 'keep')
}

At one level, over the items in scope:

  1. all filters into the items that carry every listed tag, and the remainder.
  2. then sub-queries refine the matched set, left to right. Each sub-query claims the items it places — a claimed item is removed before the next sub-query runs, so nothing leaks forward and nothing is ever duplicated.
  3. The leftover matched items (claimed by no sub-query): at a node without then they are the node's own content — leaves here, or auto-grouped by highest-frequency tag with else:'keep'. At a node with then, the branches define the partition: unclaimed items join the remainder below and follow the same else gate.
  4. The remainder (non-matching items, plus unclaimed matched items when then is present): with else:'keep' it is brought back under one sibling node marked rest: true (it doesn't carry all, so it can't sit underneath), auto-grouped by frequency inside that bucket; with else:'drop' (the default) it is filtered out.

A sub-query (branch) inside then contributes only the subtree of items it matches: its own else is purely local — it organizes that matched content ('keep' auto-groups it, 'drop' flattens it) and a branch never produces a rest node. Items a branch doesn't match fall through to the remainder above, gated solely by the owning node's else. So the outermost query's else is the single authority over the global remainder.

So else:'keep' means “be exhaustive and organized” — everything not explicitly placed lands in one clearly-partitioned “everything else” bucket a renderer can style distinctly. { else: 'keep' } alone groups the whole input by tag frequency; { all: ['x'] } is a plain filter. (rest: boolean is the deprecated 0.1.x spelling — still accepted, but not alongside else.)

Collapsing pass-through chains

The root is always a tags: [] container (a deliberate non-goal to root the tree at the query's AND-set — the uniform shape is worth one level), and nested refinements can produce chains of nodes with no leaves and a single child. If that costs your renderer depth, collapse them:

import { collapseChains } from 'tag-hierarchy';

const flat = collapseChains(tree); // pure; merges each chain into one node carrying
                                   // the combined AND-set, renumbers depths, and
                                   // preserves any rest:true marker

Ordering is tag-dependent

Sort is not a separate engine — it is choosing which tag-facet drives the order.

{ by: 'id' }                                  // stable, ascending by id (default)
{ by: 'tag', prefix: 'date-', dir?: 'asc' }   // by the value of the date- tag, lexically
{ by: 'tagFrequency', dir?: 'asc' }           // by how common an item's tags are

Lexically-sortable tags are the sort key: pad them — date-2026-02-15, prio:003 — and string order is semantic order. Items with no matching tag sort last.

Guarantees

  • Pure & deterministic — no IO, no clock, no randomness; same inputs → same frozen tree, independent of input order.
  • No duplication — an id never appears in two leaves. With else:'keep' at every filtering level (or no all), every input id appears in exactly one leaf.
  • Tags invariant — every leaf under a node carries all of that node's tags.
  • Stack-safe at scale — the data-driven grouping and the final freeze are iterative, so high-cardinality inputs don't overflow the call stack.
  • Fail-loud — a malformed query throws TagQueryError with a precise path; never a silent drop.

The kernel knows only { id, tags }. Express everything else as tags — status:open, parent:42, date-2026-02-15 — and the one mechanism covers filtering, grouping, and ordering. Visibility is the caller's job: pass only the items the viewer may see and the projection inherits the boundary.

For LLM agents

The grammar is small, order-free, and fail-loud, so a model can author a query, read the error if it's wrong, and self-correct. Two aids ship in the box:

  • LLM-USAGE.md — an example-first usage card (real data → tree outputs plus the rules small models trip on). Paste it into context.

  • queryJsonSchema — a JSON Schema (draft 2020-12) for the query, exported from the package, for constrained / structured output:

    import { queryJsonSchema } from 'tag-hierarchy';

Develop

npm run typecheck   # tsc --noEmit (strict)
npm test            # node --test via tsx
npm run build       # emit dist/ + .d.ts

Lineage

A clean-room redesign of the 2018 trokster/tagQuery (MIT), by the same author — same spirit (flat tags projected into an on-demand tree, a leading AND-set, nestable refinement, a "keep the rest" branch auto-grouped by frequency), with sound, pure semantics and a declarative grammar.

License

MIT.