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

cogplexity

v0.1.0

Published

Licence-clean ESLint rule for cognitive complexity (G. Ann Campbell's specification) on TypeScript, JavaScript and Svelte. One rule, zero runtime dependencies, no build step.

Readme

cogplexity

A licence-clean ESLint rule for cognitive complexity on TypeScript, JavaScript and Svelte — one rule, zero runtime dependencies, no build step, written from the published specification.

Status: under construction. The scoring core, rule and Svelte support land unit by unit; this README grows with them.

Why

The maintained cognitive-complexity implementation for the JavaScript ecosystem ships under a licence that forbids using outside AI to read what it produces, which rules it out anywhere coding agents read lint output. SonarCloud, meanwhile, does not analyse Svelte at all. cogplexity fills that gap with a single rule that traces every point to G. Ann Campbell's specification and is cross-checked against SonarCloud's own numbers.

Attribution

The Cognitive Complexity metric and its specification are the work of G. Ann Campbell and SonarSource: Cognitive Complexity: A new way of measuring understandability, SonarSource white paper v1.7 (2023), https://www.sonarsource.com/docs/CognitiveComplexity.pdf. This package is an independent implementation written from that paper; see PROVENANCE.md.

Installation

npm install --save-dev cogplexity

or, from the git URL at an exact tag:

npm install --save-dev github:renatomen/cogplexity#1.0.0

Either way, commit your lockfile and install with npm ci: the lockfile pins the resolved commit, while a git tag is mutable.

Requires Node >=20.19.0 and ESLint ^9.15.0 || ^10.0.0. Parsers are yours to supply: @typescript-eslint/parser for .ts/.js, svelte-eslint-parser (with the TypeScript parser nested) for .svelte.

Usage

The package exposes one rule, cogplexity/cognitive-complexity, and a flat-config helper, scoped(files, options?), that returns a single config entry running the rule as an error on the given globs (default options: the bare threshold 15). It sets no parser and ships no preset globs: you choose the files, you supply the parsers.

// eslint.config.js
import tsParser from "@typescript-eslint/parser";
import svelteParser from "svelte-eslint-parser";
import { scoped } from "cogplexity";

export default [
  { files: ["**/*.ts"], languageOptions: { parser: tsParser } },
  { files: ["**/*.svelte"], languageOptions: { parser: svelteParser, parserOptions: { parser: tsParser } } },
  scoped(["**/*.ts", "**/*.svelte"], { threshold: 15, templateThreshold: 15 }),
];

Registering the plugin by hand works the same way:

import cogplexity from "cogplexity";

export default [{ files: ["**/*.ts"], plugins: { cogplexity }, rules: { "cogplexity/cognitive-complexity": ["error", 15] } }];

Suppress a finding with ESLint's own // eslint-disable-next-line cogplexity/cognitive-complexity; the package defines no directive of its own.

The scoring function behind the rule is exported too, and needs neither ESLint nor its types:

import { score } from "cogplexity/score";

const { functions, topLevel, template, total } = score(ast, sourceText, { scopeManager });

functions holds one entry per function (kind: "function", name, depth, parent, nesting — the level its body starts at — loc, nameLoc, score, increments), topLevel the statements outside any function (kind: "topLevel"), template the Svelte template facet (kind: "template", Svelte roots only), and total the sum of the root functions and topLevel. Each increment is { construct, amount, nesting, loc }, plus operator (&& or ||) on a logicalSequence; a function's score includes everything nested inside it.

score() is the path meant for machine consumption: structured increments with locations, not prose to parse — ESLint's JSON or SARIF output carries the same breakdown only as the message text.

Options

The rule takes a bare number or an object:

| Key | Default | Meaning | |---|---|---| | threshold | 15 | Root functions scoring strictly above this are reported. A score equal to the threshold does not fire. | | templateThreshold | 15 | The Svelte template's own threshold, or false to skip the template facet for that rule instance (useful in a files-scoped override for files that forbid inline config). | | topContributors | unset | Cap the breakdown to the N largest increments by amount; unset lists every increment. |

A bare number (["error", 20]) sets threshold only; templateThreshold keeps its default.

Each finding carries one of two message ids, so an agent can tell the scores apart without reading prose: functionComplexity (located at the function's name) and templateComplexity (located at the template's first increment). The message is a header line followed by one line per increment, ordered by amount then position:

save: cognitive complexity 20 exceeds 15
+3 (incl. 2 nesting) if at 12:7
+2 (incl. 1 nesting) loop at 9:5
+1 (incl. 0 nesting) logicalSequence at 4:11
… 7 more

The template header names template. construct is one of if, elseIf, else, ternary, switch, loop, catch, logicalSequence, labelledJump, recursion for the specification's clauses and ifBlock, elseIfBlock, elseBlock, eachBlock, awaitBlock, thenBlock, catchBlock for the Svelte template; the … k more line appears only when topContributors hides some.

Svelte template scoring

Functions in a .svelte file's <script> and <script module> blocks are scored exactly as in a .ts file. The template is a second, independent score against templateThreshold; it is not part of Campbell's specification, is the package's own definition, and is never cross-checked against Sonar, which does not analyse Svelte. The two numbers never share a threshold and the template is excluded from score().total.

Nesting starts at 0 at the markup root. The rows below are the whole definition:

| Template construct | Parser node | Increment | Nesting increment | Raises nesting for children | |---|---|---|---|---| | {#if} | SvelteIfBlock with elseif: false | +1 structural (ifBlock) | yes | yes | | {:else if} | SvelteIfBlock with elseif: true | +1 hybrid (elseIfBlock) | no | yes | | wrapper of an {:else if} | SvelteElseBlock with elseif: true (holds exactly one SvelteIfBlock) | +0, transparent | — | no | | {:else} (of if or each) | SvelteElseBlock with elseif: false | +1 hybrid (elseBlock) | no | yes | | {#each} | SvelteEachBlock | +1 structural (eachBlock) | yes | yes | | {#await} | SvelteAwaitBlock | +1 structural (awaitBlock) | yes | yes (pending, then, catch) | | {:then}, {:catch} | SvelteAwaitThenBlock, SvelteAwaitCatchBlock | +1 hybrid each (thenBlock, catchBlock) | no | yes | | {#key} | SvelteKeyBlock | +0 | — | no | | {#snippet} | SvelteSnippetBlock | +0 (method-like) | — | yes | | inline function in an attribute, directive or mustache | any function node inside markup | +0 (method-like); its contents score into the template, never as a function entry | — | yes | | expression logic anywhere in markup | LogicalExpression, ConditionalExpression in a mustache tag, an attribute or directive value (class:x={a && b}, bind:), an {#each} expression or key, or a {@render}/{@const}/{@html} argument | per the specification at the current template nesting (logicalSequence, ternary) | per the specification | per the specification | | {@render}, {@html}, {@const}, {@debug} tags themselves | — | +0 | — | no |

So {#if a}…{:else if b}…{:else}{#each xs as x}…{/each}{/if} scores 1 + 1 + 1 + 2 = 5, {#await p}…{:then v}{#if v}…{/if}{:catch e}…{/await} scores 1 + 1 + 2 + 1 = 5, and a mustache {a && b ? x : y} scores 2 at the root or 3 inside an {#if}. Every increment is located at the block's opening keyword or at the operator.

Calibration

The specification is the authority; SonarCloud is a cross-check. The calibration harness (test/calibration/) proves two things about a public reference corpus analysed on SonarCloud, and passes only on exact equality:

  • per-file totals — score().total for every eligible file equals SonarCloud's cognitive_complexity file measure;
  • per-function findings — every function whose own-body score exceeds 15 has a SonarCloud S3776 issue on its name line with that score, and every S3776 issue corresponds to such a function. The own-body score is what SonarCloud reports per function: the function's increments with nested functions excluded and nesting counted from the function's own body. The rule reports a root's inclusive score instead (the paper's attribution), so the harness derives Sonar's quantity from the same increments rather than comparing the rule's number.

The corpus is renatomen/tasknotes-gantt (SonarCloud project key renatomen_obsidian-gantt), a public repository chosen so this package never depends on private code; the harness is corpus-agnostic and accepts any fixture with the same shape. The corpus identity is pinned: the calibration test fails (it does not skip) when the fixture's repository or projectKey differs from those values, and CI refuses to clone any other repository.

Fixture. calibration/fixtures/<corpus>.json records projectKey, repository, the analysed commitSha, capturedAt, the sonar block that defines the eligible file set (sources, testInclusions, exclusions, extensions), files (path → total) and issues ({ path, line, score }). The harness reads every fixture path with git show <commitSha>:<path> from your clone, so a dirty working tree or a different checked-out branch never matters; a clone that lacks the commit fails as corpus unavailable, distinct from a mismatch. A fixture path absent at the commit, or an eligible file at the commit that the fixture lacks, fails naming the path; a fixture that lists no files to compare fails too.

Run locally.

COGPLEXITY_CORPUS=/path/to/tasknotes-gantt npm run calibrate

With COGPLEXITY_CORPUS unset (as in a plain npm test), the corpus is reported as skipped with the reason — never as passed. The CI calibration job clones the corpus at the fixture's commit and runs the same command; it needs no credential, and a missing fixture fails the job rather than skipping it. The publish workflow runs the same calibration for the tagged commit before npm publish.

Refresh the fixture. Put a SonarCloud token in the SONAR_TOKEN environment variable or in a git-ignored .env, then:

node scripts/refresh-fixture.mjs tasknotes-gantt --project-key renatomen_obsidian-gantt --repository renatomen/tasknotes-gantt --sources src

The script aborts without touching the fixture on any API failure, replaces it atomically on success and prints what changed. The flags are only needed the first time; afterwards the metadata comes from the existing fixture.

Ledger. A divergence from Sonar becomes a recorded entry in calibration/ledger.json, never a chase. Entries are { kind, match, reason, addedAt, expectedDelta?, operator? }:

  • kind: "clause" — match is a construct identifier (recursion, logicalSequence, …); a logicalSequence entry may add operator (&& or ||) to name only those runs. Every clause entry naming at least one increment in a file must together explain the file's whole delta (local minus Sonar): recursion alone explains a +1 only where exactly one recursion increment exists, and a file with both a recursion increment and a || run is covered only when the ledger carries both entries and the delta is their sum. The same entries explain a per-function mismatch when the root's score minus their increments equals Sonar's score, or is at most 15 where Sonar reported no issue. A clause entry that covers no file fails the run as stale, and a file whose total already equals Sonar's although it contains a construct a clause entry records as uncounted fails as a contradiction (the message names the construct, e.g. logicalSequence(||), and the amount present); files pinned by a file entry are exempt.
  • kind: "file" — match is the repo-relative path and expectedDelta (required) is Sonar minus local, i.e. the negation of the delta the report prints. The entry covers the file only when the observed delta equals expectedDelta exactly; a later regression on that file fails naming both deltas. A file entry also accepts every per-function mismatch on that path.

Every entry carries a one-line reason.

What the ledger records today. SonarCloud counts sequences of && but not of ||, and does not increment for recursion; the specification counts both, so the rule keeps them and the ledger carries one clause entry each. SonarCloud also applies the paper's Appendix A test the way this package does — a root function with no structural increment of its own does not raise nesting for the functions inside it — except that, inside such a function, each function placed in a ternary leaks one extra nesting level for the rest of the root; the two corpus files showing that leak carry file entries.

Construct-presence report. After scoring, the harness prints how many totals and issues it compared and whether the corpus contains at least one recursion increment and at least one Appendix A promoted root (a declarative outer function), e.g. construct presence: recursion=yes declarativeOuter=no. A question about Sonar's behaviour on a construct can only be recorded as answered when that construct is present.

Compatibility and versioning

The following are the package's public contract; a change to any of them is a major version:

  • the rule name cogplexity/cognitive-complexity
  • the option keys
  • the message ids and the per-increment line grammar
  • the construct identifiers and the score() result shape
  • the scoped() helper's signature

Maintainers

  • Enable the pre-commit hook: git config core.hooksPath .githooks.
  • Releases: bump version in package.json, tag with the bare semver (git tag 1.2.3), push the tag. The publish.yml workflow runs lint, type check, tests and the pack check in the release environment, then publishes through npm trusted publishing (OIDC). One-time setup: configure a trusted publisher on npmjs.com for this repository and workflow, create the release environment with a required reviewer, and add a tag ruleset protecting [0-9]+.[0-9]+.[0-9]+ tags from creation outside releases, moves and deletion.

Licence

MIT.