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

@artshllaku/gapix

v2.2.3

Published

Static test-gap and test-quality analysis for TypeScript — finds untested error paths, unasserted symbols, and broken tests in seconds

Readme

gapix

npm CI license

Your tests pass. But do they check anything?

test('adds', () => {
  add(1, 2);        // runs every line. Checks nothing. Passes forever.
});

Coverage tools call that covered. It would still pass if add returned garbage tomorrow.

gapix reads your source and your tests, works out which functions a test actually checks, and reports the ones it doesn't:

  • error paths no test ever triggers
  • functions your tests call but never check
  • tests that pass no matter what the code does

No config. No test run. About a second.


Try it

In any TypeScript project:

npx @artshllaku/gapix check

On a shopping cart with a single test:

  assertion coverage  50%   1/2 symbols
  defects             1 critical   0 warn   0 info
  0 run, never checked · 1 never run · 1 test file

  critical  src/cart.ts:2  untested-throw-path
            total throws 'empty cart' at line 2, but no test reaching it asserts that it throws.
            → expect(() => total(…)).toThrow('empty cart')

  Biggest gaps
    applyDiscount                   src/cart.ts:5

Fix the first finding

total() throws when the cart is empty. A test calls total(), so line coverage is satisfied. Nothing checks that the error ever fires.

Add the test gapix wrote for you:

test('rejects an empty cart', () => {
  expect(() => total([])).toThrow('empty cart');
});

Run it again and the critical is gone.

Find a gap, write the test, run again. That loop is the tool.

Install

npm install -D @artshllaku/gapix

A dev dependency lands in node_modules/.bin, which is not on your shell's PATH, so a bare gapix will not resolve. Use npx:

npx gapix check

Or run it from a script, where npm puts node_modules/.bin on the PATH for you:

// package.json
"scripts": {
  "test:gaps": "gapix check"
}
npm run test:gaps

Avoid installing globally as well. With both installed, a bare gapix resolves to the global copy, which may be older. You then get unknown command for flags your local version supports. Pick one. When in doubt, which gapix and gapix --version.

Use it in CI

Fail the build on anything critical:

gapix check --fail-on critical

gapix init --ci writes the config and a GitHub Actions workflow for you.

Adopting it on an existing codebase

Any established project has findings on day one. zod has 352. Turn on --fail-on cold and the build goes red immediately, so you turn it back off.

Accept the backlog once, then fail only on what is new:

gapix check --update-baseline      # writes .gapix/baseline.json
git add .gapix/baseline.json
gapix check --baseline .gapix/baseline.json --fail-on warn

Baselined findings are still reported. They just never break the build. Anything outside the baseline does. Delete entries as you fix them.

The file is sorted and deduplicated, so it stays quiet in git. Entries match on a fingerprint that ignores line numbers, so a finding survives edits above it.

Scope a run to the pull request

gapix check --since main

Reports what the branch touched instead of the whole backlog.

GitHub code scanning

SARIF puts findings in the Security tab and inline on the pull request. No bot, no custom action:

- run: npx gapix check --format sarif -o gapix.sarif.json
- uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: gapix.sarif.json

GitHub tracks alerts across commits on the same fingerprint the baseline uses, so a finding is not raised again every time a line moves above it.

Exit codes

| exit | meaning | |---|---| | 0 | clean, or no gate configured | | 1 | gate failed | | 2 | usage error, such as an unknown flag | | 3 | bad config | | 4 | internal error |

These are stable. Pin against them.

Output formats

gapix check --format html            # interactive report, opens in your browser
gapix check --format markdown        # for a pull request comment
gapix check --format sarif           # GitHub code scanning
gapix check --format json -o - | jq  # pipe it anywhere

How it differs from coverage

Line coverage measures what ran. gapix measures what was checked.

Every exported function lands in one of four states. The gap between the first two is the entire point:

| state | meaning | |---|---| | asserted | a test checks a value that came from calling it | | exercised | a test calls it, but no check traces back to the result | | referenced | imported, never called | | untested | no test reaches it |

Assertion coverage is the share of your public functions that are asserted — how much of your code is verified, not how much of it executed.

Mocking a module demotes it to exercised. Driving a fake is not testing the real thing.

Only your public API counts. Constructors, private methods and .d.ts files are excluded, each with a recorded reason in the JSON report, so you can audit the denominator yourself.

What it catches

gapix rules lists all of them. gapix explain <rule> gives the reasoning behind one. The rules that earn their place:

| rule | what it catches | |---|---| | untested-throw-path | A function that throws, reached only by happy-path tests. Nothing else in the ecosystem checks this. | | await-async-assertion | expect(locator).toBeVisible() with no await. The promise floats, so the test passes whatever the page does. | | no-tests-without-assertions | A test with no checks. Critical when it never calls your code either, because then it cannot fail at all. A warning when it calls without checking, since a crash still surfaces. Checks inside page objects count, so Page Object suites are not flagged wrongly. | | assertion-shape-mismatch | A function returns an object, but the only check is not.toBeNull(). Read from the declared return type, so terse-but-adequate checks are left alone. | | no-focused-tests | A stray .only, silently disabling the rest of the file. | | no-tautological-assertion | expect(1).toBe(1). Inflates the count, verifies nothing. | | snapshot-only-test | A snapshot records what the code does today, not what it should do. |

Works with

Every row is backed by a fixture in tests/fixtures/. If it is listed, a test proves it.

| | checks | test structure | |---|---|---| | Jest, Vitest | expect().toBe(), .not, .resolves, expect.soft | .only / .skip / .each, xit / fit | | Playwright | web-first matchers, expect.poll, await tracking | test.describe, fixtures via test.extend | | Chai, Mocha | to.equal(), to.be.true, should | describe / context / suite | | node:test | assert.*, assert.strict.* | test(), subtests | | Type tests | expectTypeOf<T>(), assertType<T>(), @ts-expect-error, your own typed helpers | — | | Cypress | cy...should(), .and() | describe / it | | supertest | await request(app).get('/x').expect(200) | — |

Page objects and helpers are traced across files. If your spec calls checkout.submit() and the check lives inside the page object, it counts — including Playwright's fixture-based page objects.

Not supported: .vue, .svelte and .astro single-file components.

gapix runs against real repositories nightly, not just fixtures. zod, NestJS and tRPC are analysed every night, with parse failures and critical counts held to measured ceilings, so a new false positive fails the build.

Configuration

Optional. Drop a gapix.config.json anywhere above the directory you analyse:

{
  "include": ["src/**/*.ts"],
  "exclude": ["**/*.d.ts"],
  "rules": { "no-skipped-tests": "off" },
  "thresholds": { "failOn": "critical", "minAssertionCoverage": 60 }
}

Later layers win: defaults, user config, project config, GAPIX_* environment variables, command-line flags.

gapix config path shows which file was used. gapix doctor reports the resolved environment.

Commands

gapix check [path]         analyse (the default — bare `gapix` works too)
  --fail-on <severity>       exit 1 on critical|warn|info (default: never)
  --min-assertion-coverage <n>  exit 1 below n% (only when symbols exist)
  --baseline <path>          accept known findings; fail only on new ones
  --update-baseline          write the current findings as the baseline
  --since <ref>              only files changed since a git ref
  --format <fmt>             terminal|json|html|markdown|sarif
  -o, --output <path>        write there instead of the default (- is stdout)
gapix init [--ci]          create a config, optionally a CI workflow
gapix rules                list every rule
gapix explain <rule-id>    the reasoning behind a rule
gapix report               reopen the last HTML report
gapix config path|get|set  inspect and change settings
gapix doctor               report the resolved environment

gapix check --help lists every flag.

AI suggestions

Off by default. --ai adds written suggestions through OpenAI or a local Ollama.

AI never moves a number and never raises a finding. That is enforced in the architecture, not by convention. A CI gate that answers differently on identical input is broken. With AI off, two runs over the same tree are byte-identical.

Compared to mutation testing

gapix is a fast first pass, not a replacement for Stryker. Stryker mutates your code, reruns your suite, and measures empirically what your tests catch. That is ground truth, and it costs minutes to hours.

gapix reads the AST in about a second and finds the gaps that need no execution. Run gapix on every commit. Run Stryker on what it flags.

Contributing

See CONTRIBUTING.md.

Adding support for a test framework is the highest-value contribution. Each one is a single file in src/dialects/ with its own fixture, and the guide walks you through it.

Requires Node 20 or newer. MIT © Art Shllaku.