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

@titan-design/code-read

v0.3.4

Published

Versioned read API over code-graph snapshots: a contract, a per-snapshot ReadModel, browser-safe query functions, and registry commands

Readme

@titan-design/code-read

A versioned read API over @titan-design/code-graph snapshots, for three kinds of consumer: a drill-down report UI (live or from a static export), agents over MCP, and workflows. The package holds one contract, one per-snapshot ReadModel, and one pure query function per command. The daemon's commands and a browser's static dataset both answer through those functions.

Tier 2 of the titan-platform DAG (TP-184). Depends on code-graph, registry, and rpc-protocol; zod is a peer.

Two entry points

| Import | Runs in | Holds | | --- | --- | --- | | @titan-design/code-read/query | browser or Node | the contract (CONTRACT, CODE_READ_API_VERSION, zod schemas), ReadModel and buildReadModel, the ReadSource seam, QUERIES, createQueryResolver | | @titan-design/code-read | Node | everything in ./query, plus loadReadModel, createLiveSource (SQLite plus an LRU of models), and registerCodeReadCommands |

./query imports only its own files, zod, rpc-protocol, and code-graph's browser-safe ./analysis subpath, never code-graph's root. Three rules in .codewatch/check.json (code-read-query-*) and src/browser-safe.test.ts enforce that. The test bundles the subpath with esbuild for platform: "browser" and expects no warnings.

Commands (contract 0.1.8)

| Command | Args | Result | | --- | --- | --- | | api.describe | none | api, dataset, commands, newest, indexVersions, capabilities, metrics (catalogue descriptors with provenance), rules | | snapshot.list | ref?, limit (1 to 500, default 50) | snapshots, newest first | | hierarchy.get | snapshot?, root?, depth (1 to 8, default 2), metrics (default ["loc"]), baseline?, include_symbols, exclude_roles | snapshotId, baselineSnapshotId?, comparable?, nodes (flat, shallowest first, with parentId, depth, childCount, values, missing?, deltas?), truncated | | node.get | snapshot?, id, baseline?, metrics (default: every one that applies), lenses (any of exports, score, centrality, coupling, tests; default none), window (30d default, for score) | node, ancestors (repo first), childCounts, metrics (value, direction, rollup, percentile, siblingMedian, siblingRank, siblingCount, baseline?, delta?, missing?), lenses? (one key per lens asked for) | | node.resolve | exactly one of query or path, plus line? with path, limit (1 to 50, default 10) | candidates: node, score, match | | findings.list | snapshot?, baseline?, scope?, filters rule, severity, tool, provenance, kind, status (arrays, empty means all), sort (severity default, excess, value, path, rule), order (desc default), offset, limit (0 to 500, default 20), facets | snapshotId, baselineSnapshotId?, comparable?, rows (Finding), total, facets? | | finding.get | snapshot?, id, baseline?, context_lines (0 to 20, default 5) | finding, rule (with text), measured, why, excerpt (or null with excerptMissing), related (at most 10) | | node.neighbors | snapshot?, id (a stored node), direction (both default), edge_kinds, metrics (default loc, utilization), offset, limit (1 to 100, default 20) | snapshotId, node, inbound, outbound (each node, kind, weight, specifier?, values), total per side | | hotspots.list | snapshot?, baseline?, grain (file default, symbol), window (30d default, any <n>d, or lifetime), cutoff?, offset, limit (0 to 500, default 20) | snapshotId, baselineSnapshotId?, comparable?, rows (node, churn, complexity, recency, score, utilization?, baselineScore?, mark?), total | | overview.get | snapshot?, baseline?, window (30d default), cutoff (default 3000), weights (per signal, defaults from code-graph's DEFAULT_HEALTH_WEIGHTS), exclude_rules, combined (default false), reading_limit (default 6), look_limit (default 8; both 0 to 50) | snapshotId, baselineSnapshotId?, comparable?, kpis, signals (key, label, penalty, cap, measured, detail), combined?, readingOrder (node, centrality), lookFirst (node, score, churn, complexity, recency, reasons) | | changes.get | baseline (required), snapshot?, window (30d default), cutoff (default 3000), limit (rows per list, 0 to 500, default 20) | snapshotId, baselineSnapshotId, comparable, files (crossedCutoff, added), findings (new, worsened, improved, resolved), coupling (measured, added), regressions (node, before, after, delta, findings), counts | | paths.impact | paths (up to 500), snapshot?, baseline?, root? (absolute checkout directory), window (30d default) | snapshotId, baselineSnapshotId?, comparable?, rows (by status: indexed with node, complexity, hotspot (score, rank), findings, delta?; not-indexed with path; outside-repo), ranked, rollup | | packages.stats | snapshot?, packages? (package roots, up to 500; default every package the tier config declares) | snapshotId, packagesFrom (args, tiers, none), modularity, totalEdges, unassignedFiles, packages (id, name, fileCount, internalEdges, outgoingEdges, incomingEdges, cohesion, instability, abstractness, band, flags, layer), crossEdges (from, to, edges, intensity, flag) |

Arguments are snake_case and results are camelCase. snapshot and baseline take an id, a digit string, or a ref name (that ref's newest snapshot). The rest of the design's 14 commands arrive in later minor versions of the contract.

The hierarchy

code-graph stores files and symbols only, so ./query synthesizes the rest per snapshot:

  • Ids. The repo is "". A directory is its path plus /, such as src/util/; no file id ends in /. Module and external nodes are not in the hierarchy.
  • Class level. A symbol named Job.run hangs under Job when that symbol exists, else under the nearest existing scope. A bare name (snapshots before code-graph 0.14.0) hangs under the smallest symbol whose span encloses it, else under its file.
  • Rollups. A directory's value, or a file's value for a symbol-only metric, combines the stored values below it by the catalogue's rollup rule: sum, max, or mean. A file metric rolls up from files only, so nothing counts twice. absent: "zero" counts a missing row as 0; absent: "exclude" leaves it out.
  • Why a value is null. missing says why: no-rollup (the rule is none, as for commit and author counts, bus factor, and fan-in; directory-level history is TP-233), not-measured, not-applicable (such as loc on a symbol), or not-in-snapshot.
  • Roles. exclude_roles drops files of those roles from the rows, the rollups, and the baseline alike.
  • Deltas. Baseline nodes match by id only; alias following is TP-187. comparable is false when the two snapshots have different index versions.
  • Size. hierarchy.get returns at most HIERARCHY_ROW_CAP (5,000) rows breadth-first and sets truncated. childCount always counts every child, symbols included.

node.get percentiles are the share of same-kind nodes whose value is at most the node's, 0 to 100. Siblings are the same-kind children of the same parent, the node included; rank 1 is the largest value. Neither number knows which way is worse. Read direction on the same metric before labelling anything "top" or "best".

Worked example. A file with loc: 3 among sibling files with 0, 1, 2, and 2 lines gets siblingRank: 1, and 7 of the snapshot's 10 files have at most 3 lines, so percentile: 70. loc is direction: "higher-worse", so rank 1 is the file with the most lines. It is the worst offender in its directory, not the best file. For bus_factor_30d (direction: "lower-worse"), rank 1 is the file with the most authors covering its churn, which is the safest file. For a neutral metric such as churn_30d, rank 1 only means "largest".

node.resolve ports codewatch's rankSearch cascade over directories, files, and symbols, case-insensitive: exact id or name 100, id suffix after / or # 80, name prefix 60, id substring 40, name substring 30, ties by id. One addition for qualified names: a symbol whose last segment equals the query, such as run for Task.run, scores as a suffix. A path with a line, or a query of the form path:line, maps each matching file to the innermost symbol whose span holds the line.

Findings

Until the findings store exists (design gap G11), every finding is a check-rule violation derived on read. The live source runs the product's rules through code-graph's own snapshotViolations when it loads a snapshot, so findings.list returns exactly what graph check reports. Each row says provenance: { kind: "derived", source: "check/<rule>" } and tool: "check". Stored findings, verdicts, and themes will arrive behind the same Finding schema.

  • Ids. A finding's id is code-graph's violationKey: rule|node or rule|node|destination. Treat it as opaque. It stays the same across snapshots while the rule id, the node id, and the destination are unchanged. A moved file gets a new id until rename-aware keys land (TP-187).
  • Order. sort picks the primary key and order flips only that key. Ties always break the same way: severity (error, warning, info, then unknown), then larger excess, then node path, rule, and id. The id is unique, so the order is total and offset pages never repeat or skip a row. Rows with no excess or value sort last in either direction.
  • Excess. value / threshold for a maximum rule and threshold / value for a minimum rule, so larger is always worse. Import rules have no threshold, so excess is null.
  • Facets. With facets: true, counts per rule, severity, tool, provenance, kind, and child, plus status with a baseline. They count every row that passed the filters, so each facet sums to total. child is the scope's child (a directory id or a file) that holds the finding, which is what a drill-down view shows next. limit: 0 with facets: true gives headline counts in one call.
  • Scope. A directory scope holds everything under it, a file scope holds the file and its symbols, and the repo ("", the default) holds everything.
  • Status. With baseline, each row is new, carryover, worsened, or improved (by excess), and findings that no longer occur come back from the baseline as resolved, with the baseline's snapshotId. A status filter without a baseline is DATAERR.
  • Excerpts. finding.get shows the flagged lines plus context_lines either side, clipped to the file and capped at EXCERPT_LINE_CAP (80) lines with truncated: true. Edges carry no line numbers, so an import finding's flagged line is the one that names the import's specifier in quotes; the live source finds it when it loads the snapshot. A whole-node finding, such as a metric finding, uses its node's span when the node has one: a symbol finding gets the symbol's lines as range and highlight. A node with no span, such as a file, gets no range, no highlight, and an excerpt that starts at line 1. The live source reads the working tree under repoRoot and serves a file only when its hash equals the snapshot's fingerprint. Otherwise excerpt is null and excerptMissing says changed-since-snapshot. A static source serves the windows it exported and says not-in-export for the rest.
  • Neighbours. node.neighbors needs a stored node: a file, symbol, module, or external. A synthesized directory is DATAERR. Each side is ranked by edge weight (code-graph's reference count), heaviest first, then by neighbour id and edge kind, and paged on its own. Empty edge_kinds means every kind except references for a non-symbol node, because references edges are the symbol layer.

Trap: a derived finding is not a record. It exists only while its rule, at its current threshold, still fires. Change a rule's threshold, rename a rule, or remove it, and its findings vanish from every snapshot, old ones included, and their ids never come back. A finding's status also depends on the baseline you pass: the same row is carryover against one snapshot and new against another. Do not persist a derived id as if it were durable until the findings store lands.

Hotspots

hotspots.list computes nothing of its own. It runs code-graph's report derivations from the browser-safe @titan-design/code-graph/analysis subpath, so the daemon and a static dataset rank alike.

  • File grain is topHotspots: round(churn_<window> × complexity × recency_<window>), where complexity is cognitive_max, else cyclomatic_max. A file with no churn or no complexity has no row, and generated files are left out.
  • Symbol grain is buildBlastRadius: utilization × complexity × file churn, with the symbol's own symbol_cognitive, else its file's. It applies no recency, so recency is 1. Symbols of generated files are left out.
  • Marks come from computeReportDrift over every row: new when the node had no row at the baseline, worsened when its score rose. baselineScore is null for a new row.
  • Cutoff is the caller's policy, such as the dashboard's 3000. It filters before paging, and total counts the rows it keeps. Marks do not depend on it.

Overview

overview.get answers "where do I look first" with named attention signals, not a risk or defect score. It also reuses code-graph's derivations from ./analysis:

  • Signals are computeHealth's components, weighted and capped by the caller's weights: files over cutoff, open findings (new and carried over weigh apart), max complexity over budget, and hidden coupling, which is unmeasured until co-change pairs are stored. combined (100 minus the penalties) is optional and secondary.
  • Reading order is topCentralFiles: PageRank over files and structural edges.
  • Look first is the file-grain hotspots, highest first. Each row names its reasons: over-cutoff, findings, or else churn-complexity.

Node lenses

node.get takes optional lenses. With none, the result has no lenses field and is the same as before. Each lens reuses a code-graph derivation from ./analysis, and answers null on a node kind it does not describe: score describes files and symbols, the rest describe files only.

  • exports is buildHotExports: the file's top 8 exported and top 8 internal symbols, each with utilization, its own cognitive complexity, and how many files reference it (computeSymbolConsumers).
  • score is the node's row in hotspots.list at its grain for window: the score, its factors, and its rank among non-zero scores. A node scoring 0 has no factors and no rank.
  • centrality is topCentralFiles with no limit, the ranking behind overview.get's reading order, so every file in it has a rank. Generated code is left out and gets null.
  • coupling is measured: false until co-change pairs are stored.
  • tests lists tests linked by path convention (linkTestsToSources with no co-edit pairs) beside the index's linked_test_count, which also counts co-edit links.

Changes

changes.get answers "what moved since the baseline". It recomputes nothing code-graph already derives:

  • Files come from computeReportDrift over every file-grain hotspot. crossedCutoff holds files below cutoff at the baseline and at or above it now; added holds files the baseline does not hold, generated files left out.
  • Findings are bucketed by bucketViolations, the store-free core of diffCheckResults: new, worsened or improved by excess as findings.list statuses are, and resolved. Ids match as they are; following renames is TP-187.
  • Coupling is unmeasured (measured: false) until co-change pairs are stored.
  • Regressions are files whose score rose that carry an open finding now.
  • Across index versions comparable is false and every list is empty.

Path impact

paths.impact answers "what do these files weigh", for a set such as the files a change touches. It ranks nothing a second way:

  • Complexity is the factor the file hotspot score multiplies (hotspotComplexityOf: max cognitive, else max cyclomatic), read even for a file with no churn.
  • Hotspot is the file's row in hotspots.list at window: rank is its row number there and ranked the list's total. A file scoring 0 has rank null.
  • Findings are the open findings on the file or a symbol in it, worst first.
  • Delta, only with baseline: score, complexity, and findings counts against the baseline, findings bucketed by bucketViolations as changes.get buckets them, and each open finding gets a status. Without a baseline delta is absent; across index versions it is null.
  • Paths are repo-relative. A leading ./, . and .. segments, and doubled slashes are normalized; an absolute path counts only under root. A path the snapshot holds no file for is a not-indexed row and one outside the repo an outside-repo row, never an error.
  • The rollup counts each status, sums scores and open findings, and takes the top complexity and best rank; with a comparable baseline it sums the deltas, a new file adding its whole score.

Package stats

packages.stats is code-graph's computePartitionQuality over a set of package roots, the numbers codewatch's graph arch --health prints:

  • Roots are repo-relative path prefixes. Given as packages, those; otherwise every root a layered-deps rule names, the repo's tier config. packagesFrom says which, and is none when neither names one. A file sits in the longest root it falls under; the rest count as unassignedFiles.
  • Edges are the structural layer the store reads by default, so references and calls edges are left out, as are test and fixture files, as computeArch leaves them out.
  • Per package: file count, internal, outgoing, and incoming edges, cohesion, instability, abstractness (the share of types files), the measured band, and flags.
  • Layer is declared, not measured: the root's tier in the first layered-deps rule that names it, 0 the lowest. A root no rule names has layer: { status: "undeclared" }.
  • Cross edges are the package-to-package counts with their intensity and flag, and modularity is the partition's Newman-Girvan Q.

Serving the commands

import { openCodeGraph, loadCheckRules } from "@titan-design/code-graph";
import { registerCodeReadCommands } from "@titan-design/code-read";
import { startDaemon } from "@titan-design/daemon";
import { createRegistry } from "@titan-design/registry";

const registry = createRegistry();
const rules = await loadCheckRules(".codewatch/check.json");
registerCodeReadCommands(registry, {
  openStore: () => openCodeGraph(".codewatch/graph.db"),
  rules: () => rules,
  repoRoot: gitToplevel,
});
await startDaemon({ registry, createContext: () => ({ warnings: [], format: "json" }), version, stateDir, toolPrefix: "codewatch__" });

rules must return the same array while the rules are unchanged: the live source compares arrays by identity and drops every cached model when a different one comes back. repoRoot is the git toplevel the snapshots were indexed from; leave it out and finding.get returns no excerpts. Deriving findings adds a rule-evaluation pass to each model load, about 25 ms on titan-platform's own index.

The daemon then answers POST /rpc/api.describe and the MCP tool codewatch__api__describe with the same envelope as an in-process invokeCommand. The daemon lists every registered command as a tool. To follow the design and put only the agent-shaped commands on MCP, register only the defineCodeReadCommands(source) entries whose names are in AGENT_COMMANDS on the registry that serves MCP.

Answering without a daemon

createQueryResolver(source) returns (name, args) => JsonEnvelope. It validates raw args against the contract, runs the query, and never throws. It uses the same codes as the daemon: DATAERR for bad args, NOINPUT for a missing snapshot, UNAVAILABLE for a command the source does not serve, and USAGE for an unknown name. The resolver fits @titan-design/rpc-client's static resolve option. Any ReadSource works, for example one decoded from a static dataset.

Versioning

CODE_READ_API_VERSION is the contract's semver. contract.lock.json stores every command's args and result as JSON Schema. src/contract-lock.test.ts fails when the schemas change and the version does not. Its failure message says whether the change is breaking and names each breaking path. After bumping, regenerate the lock:

UPDATE_CONTRACT_LOCK=1 pnpm vitest run packages/code-read/src/contract-lock.test.ts

The lock leaves out descriptions, so a documentation edit needs no bump.