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

adr-lens

v0.4.0

Published

Make architecture decision records readable. Discover, browse, and audit ADRs from the terminal.

Readme

adr-lens

Make architecture decision records readable.

ADRs are easy to write and hard to read. Once a service has eighty of them, the corpus stops being documentation and becomes an archive: you cannot tell which decisions still hold, which were quietly replaced, or which three you actually need before touching the code.

adr-lens reads a directory of ADRs and answers the questions you actually have.

adr-lens web --open        # a readable web page for the whole corpus
adr-lens map               # where do I start?
adr-lens list --live       # what is currently true?
adr-lens show 65           # read one, with its citations
adr-lens browse            # explore interactively
adr-lens lint              # what is broken or stale?

Try it without a repo of your own

The repo ships an invented 13-record corpus:

git clone https://github.com/Alexanderdunlop/adr-lens && cd adr-lens
npx adr-lens web -C examples/billing-service --open

It covers the awkward cases on purpose: both title layouts, YAML frontmatter, a hard-wrapped status, a full supersession chain and a partial one, a rejected decision, tables, a mermaid diagram, and a link to a non-record file.

Install

npx adr-lens web --open      # no install
npm install -g adr-lens      # or keep it around

Requires Node 24+.

Everything runs on your machine and reads your local files. Nothing is uploaded, and there is no service or account involved — which is the point, since ADRs usually live in private repos. The page it writes is a single file with no external requests, so where that file goes afterwards is entirely your choice: open it locally, commit it, or host it. (adr-lens web --serve runs a server, but a local one, bound to loopback, and only while you leave it running.)

What it does

Finds your records without configuration. It walks the tree looking for directories named adr, adrs, decisions, decision-records, architecture-decisions, or architecture-decision-records, and skips node_modules and friends. Point it at one repo or at a folder full of them.

Parses the formats people actually use. The adr-tools layout (# 2. Title with a ## Status section), the bold-field layout (# ADR-001: Title with **Status:** Accepted), and YAML frontmatter all work. Status is normalised from free text, so Accepted, accepted ✅, and Proposed → Accepted all land in the same place.

Extracts the decision. Every listing can show one sentence answering "what did we decide?", lifted from the ## Decision section — skipping the sub-headings and bolded pseudo-labels that decision sections tend to open with.

Builds the citation graph. Cross-references between records become a graph, so adr-lens map can lead with the most-cited decisions. In a corpus of ninety records, the handful everything else cites is the reading list.

Distinguishes partial supersession. "Superseded in part by ADR-0065" is not the same as "Superseded by ADR-0065" — the first record is still in force. Both phrasings are recognised, along with supersedes parts of, partially superseded by, and the pattern where a status names what died and adds that the rest "remains in force". Relations are mirrored, so only one side has to say it.

Commands

web — the readable page

Writes the whole corpus as one self-contained HTML file: no build step, no server, no external requests. Open it from disk, commit it, or publish it.

adr-lens web --open                 # write <scope>-decisions.html and open it
adr-lens web --serve --open         # keep it open while writing; the tab reloads itself
adr-lens web -o docs/decisions.html
adr-lens web -C ~/code --open       # every repo under a directory, grouped

--watch rewrites the page whenever a record changes. It reloads the whole corpus each time rather than patching one record, because adding a record changes the citation graph and the rankings for every other record. Reload the page to see a rebuild — your scroll position is restored, so you land where you were.

--serve adds a small localhost server and pushes that reload for you, so saving a record updates the tab. It is additive: the file is still written to disk, and the reload client exists only in the copy served over HTTP — what you commit or send to someone is the same plain page as before. It listens on loopback only, prints its URL, takes a free port if 4230 is busy (--port to choose), implies --watch, and stops with ctrl-c.

The page has two halves:

  • The register — every record as one row: number, status, title, the date, and the one-sentence decision. Sorted newest first, with a toggle for Number or Most cited; undated records always sort last. Type to filter across titles, decisions, and body text; filter to current only or by status. / focuses the search, j/k step through results.
  • The reading view — the decision stated once, up front, before any context. Then the record itself: proper measure and line-height, tables that scroll in their own container, mermaid diagrams drawn as diagrams, and cross-references as links you can click. Replaced records say so at the top and point at what replaced them; partly-replaced records say that instead, because they are still in force.

Sharing one decision. Sending someone the whole page and asking them to find ADR-0009 in it does not work. Each record carries four ways to send just itself:

| | | |---|---| | Copy link | the record's own URL — the hash route already identifies it | | Copy markdown | the record's source, for pasting into Slack, a ticket, or a review | | Copy citation | ADR-0009 Single-store idempotency gate (accepted, 2026-04-18) | | Download PDF | adr-0009-single-store-idempotency-gate.pdf, in one click |

The PDF. One click, straight to a file — no print dialog, which is aimed at a printer and cannot be scripted. The page carries its own PDF writer, so this works offline, from file://, with no dependency and nothing to install.

It is a real document, not a screenshot: the text is selectable and searchable, files are tens of kB, and it uses the PDF base-14 fonts, so nothing has to be embedded. Headings, lists, tables, code, and block quotes all come across. External URLs are spelled out in the text, because a link in a PDF someone was emailed is not necessarily clickable — in-page routes are not, since ADR-0009 already says everything the route would. A table never splits a row and repeats its header across a page break; long code lines and URLs wrap rather than being cut at the paper edge. Every page is numbered and carries the service and the record number in its footer, which is the one thing paper needs that the screen does not. Mermaid diagrams come out as their source text.

⌘P still works, and has its own stylesheet — the register and the controls drop away, and it comes out black-on-white even if you were reading in the dark theme. The overview prints too, as a one-page summary of the corpus.

There is no "download markdown" button, deliberately: it would hand you a file you already have, by way of a file manager. Copy markdown does it in one step.

It adapts to light and dark, collapses to one column on a phone, and needs no network. A 100-record corpus is about 2 MB.

map — where to start

Leads with the most-cited records, then supersession chains, then the records nothing references.

$ adr-lens map -C ../billing-service

Decision map
════════════
42 records · 37 current · 48,010 words · ~218 min to read all

By status
  ● Accepted      34  ██████████████████████████████
  ◐ Proposed       3  ███
  ⊘ Superseded     5  ████

Start here — most cited
  These are the decisions everything else builds on.

   14 ← ● 0002 Idempotent batch settlement
        We adopt a single-pass settlement model built on Lambda + SQS.
    9 ← ● 0024 Single-store idempotency gate
        buildInvoiceId(tenantId, customerId, periodId?) produces a …

Supersession chains
  Where the thinking changed. Read the last one.

  0007 → 0012 → 0031  Retry conflicting writes
  0019 → 0024        Single-store idempotency gate

list — triage

One line per record: number, status, title, inbound citations, age. Superseded titles are struck through.

adr-lens list                        # everything, by number
adr-lens list --live --sort influence -V   # what's current, most-cited first, with decisions
adr-lens list retry                  # only records matching "retry"
adr-lens list -s proposed            # only proposed
adr-lens list -g                     # grouped by source directory

show — read one

Renders a single record to the terminal: headings, wrapped prose, lists, tables, code blocks, and blockquotes — plus its relations and everything that cites it.

adr-lens show 65                     # by number
adr-lens show idempotency-gate       # by filename fragment
adr-lens show 65 --summary           # header and decision line only
adr-lens show 65 --sections decision,consequences
adr-lens show 65 -u                  # print link destinations

browse — explore

An interactive two-pane browser: filterable list on the left, rendered record on the right.

| key | action | | --- | --- | | ↑ ↓ / j k | move between records | | space / b | page down / up | | g / G | first / last | | enter | focus the record pane | | tab | switch pane | | esc | back to the list | | / | filter — type, enter to keep, esc to drop | | c | clear the filter | | s | cycle status filter | | L | toggle current-only | | x | jump to a related record | | [ ] | back / forward through jumps | | e | open in $EDITOR | | ? | key help | | q | quit |

lint — audit

Exits non-zero only on errors, so it is safe in CI.

| rule | severity | what it catches | | --- | --- | --- | | duplicate-number | error | two records claiming the same number in one directory | | broken-link | error | a local link pointing at a file that does not exist | | supersession-cycle | error | two records superseding each other | | dangling-reference | warn | a supersession naming a record that cannot be found | | stale-status | warn | reads as accepted, but another record supersedes it | | superseded-without-target | warn | marked superseded with no identifiable replacement | | no-status | warn | missing or unrecognised status | | no-date | info | no date | | number-gap | info | gaps in numbering — usually a deleted record | | missing-section | info | no ## Context or ## Decision | | orphan | info | cites nothing and is cited by nothing |

adr-lens lint          # errors and warnings
adr-lens lint --all    # include informational checks
adr-lens lint --json   # machine-readable

search — rank by relevance

adr-lens search retry counter    # every term must match
adr-lens search 34               # exact number match ranks first

Options

-C, --root <path>    directory to search (default: cwd)
    --depth <n>      how deep to look for ADR directories (default 6)
-d, --dir <text>     only records whose path contains <text>
-s, --status <list>  accepted, proposed, rejected, deprecated, superseded, unknown
    --live           hide superseded records
-w, --width <n>      columns to render into (default: terminal width, capped at 100)
    --sort <key>     number | influence | date | title | length
-n, --limit <n>      show at most n records
-V, --verbose        add the decision line under each row (list)
-g, --group          group by source directory (list)
-u, --urls           print link destinations (show)
    --summary        header and decision line only (show)
    --sections <l>   comma-separated section names (show)
    --top <n>        entries per section (map)
-a, --all            include informational findings (lint)
    --json           machine-readable output
    --no-color       disable colour

Programmatic use

The core carries no terminal concerns, so it can back other surfaces — a web view, an editor plugin, or an MCP server exposing the corpus to an agent.

import { loadCorpus, decisionLine, lintCorpus } from 'adr-lens';

const corpus = await loadCorpus('./');

for (const adr of corpus.adrs) {
  console.log(adr.number, adr.status, decisionLine(adr));
}

const findings = lintCorpus(corpus, { all: true });

loadCorpus returns records with resolved relations, inbound and outbound citations, and full-supersession fields. renderMarkdown is exported separately if you want the terminal renderer without the CLI.

Development

pnpm install
pnpm dev map -C ../some-repo   # run from source
pnpm test                      # vitest
pnpm check-types               # tsc --noEmit
pnpm lint                      # biome
pnpm build                     # tsdown → dist/

Releasing

Versions are managed by changesets. Add a note in the same pull request as the change:

pnpm changeset

On merge to main, CI opens a "chore: version packages" pull request that bumps the version and writes the changelog. Merging that pull request publishes to npm. A change with no changeset ships no release — correct for refactors, tests, and docs.

prepublishOnly runs types, tests, and the build before anything leaves, so a red build cannot publish.

Requires an NPM_TOKEN repository secret with publish rights.

Licence

MIT