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

locator-registry

v0.3.0

Published

Ground-truth UI element locators for AI coding assistants, plus a Locator Risk Score that flags which locators are likely to break your tests. Local CLI + MCP server, no hosting required.

Downloads

31

Readme

Locator Registry

Your AI coding assistant guesses selectors and the test fails. Locator Registry gives it the real ones from your code, verified against your running app — and scores which elements keep breaking.

Two features:

  • Locator MCP — a local MCP server that gives Cursor, Claude Code, or any MCP-compatible assistant ground-truth locators for your app's elements, sourced from your own code and confirmed against your running app.
  • Locator Risk Score — combines git churn on your test repo with CI failure history to flag which locators are most likely to break next, with evidence for every score.

Runs entirely locally: a CLI plus a local stdio MCP server. No account, no hosting, no domain. Nothing leaves your machine.

Status: Gates 1–3 pass automated validation (parser-validate, risk-validate, cli-validate) against test-app ground truth and calibration fixtures. 137 tests. Human QA review on a production repo is still recommended. See Limitations.

Install

npm install -g locator-registry

Or run it without installing:

npx locator-registry scan ./my-app

Playwright is a dependency (used for verify); its browser binaries install automatically the first time you run a command that needs them.

Feature 1 — Locator MCP

1. Fingerprint your app (optional, informational)

locator-registry fingerprint ./my-app

Detects framework, router type, language, UI library, test framework, package manager, monorepo layout, and your existing test-attribute convention (data-testid, data-qa, etc.).

2. Scan

locator-registry scan ./my-app

Walks your app's routes — Next.js App Router, Pages Router, React Router (both the createBrowserRouter([...]) config API and the <Routes><Route> JSX API, including nested routes, dynamic params, and catch-alls), Vue Router (createRouter({ routes: [...] }), including nested/dynamic routes and lazy-loaded () => import(...) components), or SvelteKit's file-based routing (src/routes/, +page.svelte/+layout.svelte, [param]/[[optional]]/[...rest] segments) — parses each page to an AST, and extracts every interactive element with ranked locator candidates. Also writes a repository manifest (repo-manifest.json: file inventory, entry points, local import graph) and a bounded component composition tree per page for React (JSX import following), Vue/Svelte (local .vue/.svelte import following), and Angular (selector-map matching), up to depth 8. Writes .locator-registry/ in your current directory — an index.json manifest plus one JSON file per route. Nothing is sent anywhere.

React Router has no file-system routing convention, so route definitions are found by scanning the whole project for createBrowserRouter/createHashRouter/createMemoryRouter calls and <Routes> JSX trees, wherever they live. A route's component only resolves if it's a traceable local import or declared in the same file — never guessed, so a component from a UI library or a dynamic reference is honestly skipped rather than misattributed. Next.js detection is gated on an actual next dependency in package.json, not just on an app//pages/ folder existing — plenty of plain React Router projects use a pages/ folder purely for organization, and that's not Next.js. SvelteKit detection is gated the same way, on an actual @sveltejs/kit dependency.

Vue detection works the same way, framework-appropriate: components declared as .vue single-file components are told apart from native HTML elements using Vue's own compiler AST (no naming-convention guessing), and a component reference resolves through Vue's automatic attribute fallthrough — a data-testid (or other locator-relevant attribute) passed at the call site and not declared as a prop lands on the component's own root element, the same way it would at runtime.

Svelte works differently, and deliberately so — verified by compiling real Svelte 5 components rather than assumed: unlike Vue, Svelte does not forward extra attributes to any element automatically. Forwarding only happens where the component author explicitly spreads a rest-props object ({...$$restProps} in Svelte 4, or {...rest} from let { a, ...rest } = $props() in Svelte 5) onto some element in their own template — wherever that is, root or not. A component reference resolves by finding that spread in the AST and merging the call site's own attributes into it; if no such spread exists, the call site's attributes genuinely aren't reachable, and that's reported as a caveat rather than silently guessed.

3. Verify against your running app (optional but recommended)

locator-registry verify --url http://localhost:3000 [--storage-state auth.json] [--evidence]

Opens a browser, visits each discovered route, and confirms each element actually exists in the live DOM — marking it RESOLVED, CONDITIONAL (in source but not observed — likely a feature flag or permission gate), or UNRESOLVED. --storage-state reuses whatever Playwright auth state your own test suite already produces, so this tool never needs to understand your login flow. --evidence captures a screenshot and DOM snapshot per route.

4. Point your AI assistant at the MCP server

Generate a starter MCP config (merges with any existing servers):

locator-registry init-mcp --app ./my-app          # writes .cursor/mcp.json
locator-registry init-mcp --app ./my-app --claude # writes .mcp.json

Or add manually to your MCP client's config (e.g. Claude Code's .mcp.json, Cursor's MCP settings):

{
  "mcpServers": {
    "locator-registry": {
      "command": "npx",
      "args": ["locator-registry-mcp", "/absolute/path/to/my-app"]
    }
  }
}

The server exposes six tools:

| Tool | Purpose | |---|---| | get_page_map() | Every route discovered, with resolution status | | list_elements(page) | Every element on one route, with status and confidence | | get_page_composition(page) | Bounded component tree for one route (React/Vue/Svelte/Angular) | | get_locator(page, element_description) | Ranked candidates, confidence, evidence, and ready-to-use Playwright/Selenium/Cypress syntax | | get_risk_report({ top?, route? }) | Ranked risky elements with evidence (requires risk and/or ingest first) | | report_result(locator, outcome, error?) | Best-effort — lets the agent report back whether a locator actually worked |

Ask your assistant to write a test for a page. It calls get_page_map and get_locator instead of guessing.

Feature 2 — Locator Risk Score

1. Analyze git churn on your test repo

locator-registry risk ./my-tests [--top 10] [--exclude-bare-tags]

Reads git log over your test files and scores each locator by how often it's changed, over what time span, by how many authors. No CI integration needed — this alone produces a ranked list within minutes of connecting.

2. Ingest CI test results

locator-registry ingest ./artifacts/junit.xml
# or, for Playwright specifically:
locator-registry ingest ./artifacts/playwright-report.json

Parses JUnit XML (the format every major CI system and test framework already emits) and extracts the locator from each failure message. Run this after every CI run — results accumulate.

JUnit XML undercounts real instability wherever retries are configured — confirmed empirically, not assumed, across all three frameworks. A flaky-on-purpose test (fails once, passes on retry) was run for real through Playwright's own reporter, Cypress's mocha-junit-reporter, and pytest's built-in --junitxml (driving Selenium), each with retries enabled. All three wrote the retried-then-passed test as a completely clean pass — failures="0", no <failure> element, no indication a retry ever happened. This isn't a bug in any one tool; JUnit XML's schema has no concept of "attempt" at all, so nothing that emits it can represent a recovered failure. Since retries exist specifically to absorb exactly this kind of flakiness, a locator that's genuinely intermittent can score as if it never failed once.

For Playwright, use --reporter=json instead — it preserves every attempt, including ones a test recovered from, and locator-registry ingest reads it directly, counting each retried-but-recovered test execution as one instability event (not one per retry attempt — three retries in one CI run is one flake, not three).

Selenium and Cypress ingestion is JUnit-only, and investigation found no zero-effort fix for either — confirmed by testing, not assumed. For Cypress: neither mocha-junit-reporter nor mochawesome (a richer JSON reporter) records a retried-then-passed test as anything but a single clean pass, and even Cypress's own Module API — which has a field (test.attempts) specifically documented for this — only ever returned one attempt in testing, despite the retry demonstrably happening (timing matched the retry delay exactly). For Selenium via pytest + pytest-rerunfailures: pytest-json-report's output has the same blind spot, and --rerun-show-tracebacks (a flag that does print the failed attempt's traceback) only affects console text output — the JSON artifact itself is unchanged. Getting equivalent visibility for either would mean the customer writing custom test-repo instrumentation (a pytest hook, a Cypress plugin), not picking a different built-in reporter flag — out of proportion for what this tool promises (zero code changes in the customer's CI). Left as a known, permanent limitation for these two frameworks rather than forced.

3. Get the combined risk report

locator-registry risk-report [--top 10] [--json] [--exclude-bare-tags]

Validate Gate 1 parser accuracy (static ground-truth match + optional runtime verify against test-app):

locator-registry parser-validate [--json] [--no-verify]

Validate Gate 2 calibration (automated QA ranking check against test-app + synthetic evidence):

locator-registry risk-validate [--json]

Validate Gate 3 CLI usefulness (automated rubric — actionable locators, transparency flags, evidence sections, JSON mode, clear errors):

locator-registry cli-validate [--json]

Or run the full local pipeline in one command (scan → optional verify → optional risk → optional ingest → risk report):

locator-registry pipeline \
  --app ./my-app \
  --verify-url http://localhost:3000 \
  --storage-state auth.json \
  --tests ./e2e \
  --junit ./artifacts/junit.xml \
  --top 10

Joins your scanned registry against both churn and failure history, and prints a ranked list like:

Locator Risk: 47/100          Confidence: HIGH

  "Apply promo code" button — /checkout
  Source: app/checkout/page.tsx:21

  Evidence
    • locator changed in 2 commit(s), 2 author(s), last changed 2026-08-11 (churn score 2.6)
    • 3 locator-related failure(s) in the last 30 days, last failure 2026-08-11

  Cause
    Located by accessible role/label (good practice) — but the element's accessible name may be changing, and there's no dedicated automation attribute yet.

  Fix
    Add data-qa="checkout-apply-promo-code" as a supplementary stable anchor        [1-line PR]

Also available as an MCP tool, get_risk_report({ top?, route? }), so your assistant can check an element's risk before it relies on that locator.

How confidence and risk scores are computed

Both scores are documented, evidence-backed approximations of formulas described only in prose in the original design spec — not literal reproductions. Every score is always returned with the evidence behind it; neither tool reports a number without justification. See src/confidence.ts and src/risk-score.ts for the exact formulas and the worked examples they're checked against.

Limitations

  • Static analysis sees JSX/templates, not the rendered DOM — deep composition through component libraries may be unresolvable without verify.
  • Duplicate accessible names on the same route (e.g. repeated "View" links in a table) are flagged AMBIGUOUS at scan time and refused as a single top locator by MCP — scope by row or add a test attribute.
  • Conditional rendering and feature flags mean an element may not exist at runtime even if it's in source.
  • Risk score constants are calibrated against the spec's worked example and an automated Gate 2 fixture (fixtures/gate2/) — not yet validated by a live QA team on a production repo.
  • JUnit failure-message extraction covers Playwright, Selenium (JS/Java/Python/C#-shaped messages), and Cypress — an unrecognized format is left honestly unextracted, never guessed.
  • JUnit XML ingestion undercounts instability wherever CI retries are configured — see the retry note under Feature 2, step 2. Playwright JSON ingestion closes this gap for Playwright; Selenium and Cypress are still JUnit-only and still have it.
  • Angular is supported for route discovery and template analysis (standalone components with provideRouter / RouterModule). Vue 3 and Svelte 5 (SvelteKit) are also supported.
  • React Router's file-based routing convention (Remix / React Router v7's newer file-route API) is different from the config-object and JSX APIs supported here, and isn't covered.
  • Vue: attribute fallthrough is only traced onto a component's single root element, matching Vue's own inheritAttrs behavior — a multi-root (fragment) component, or an interactive element nested below the root, is not covered. Like Svelte, only one hop of forwarding is traced.
  • Svelte: only one hop of {...rest} forwarding is traced — a component that receives an attribute and re-forwards it into a second child component (rather than applying it directly) won't have that second hop traced back to the original call site. Plain Svelte apps with a hand-rolled router (no SvelteKit) aren't covered — there's no file-based convention to walk.

Development

npm run build          # compile TypeScript
npm test                # run the test suite (includes Gate 1–3 validation)
npm run typecheck:tests # typecheck test files
locator-registry parser-validate  # Gate 1 parser + verify check
locator-registry risk-validate  # Gate 2 calibration check
locator-registry cli-validate   # Gate 3 CLI usefulness check

License

ISC