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

candor-ts

v0.7.5

Published

candor for TypeScript — per-function side effects, transitively, with a policy gate (candor-spec 0.5)

Downloads

5,402

Readme

candor-ts

candor for TypeScript: per-function side effects, transitively, with a deterministic policy gate. candor-ts resolves every call through the TypeScript compiler API and reports, for each function in your project, which effects it can reach — Net, Fs, Db, Exec, Env, Clock, … — including effects inherited through any chain of calls across files, with an honest Unknown wherever resolution fails (a callback value, an any-typed callee — never silently pure). A candor-spec implementation, sibling of the Rust and JVM engines.

Site: candor.poly.io — the measured case in five minutes.

npm install   # typescript + @types/node

node scan.mjs <project-dir>                 # tsconfig.json honored; tests excluded; writes
                                            #   <dir>/.candor/report.json + .callgraph.json
node scan.mjs . --policy .candor/policy     # the §6.2 gate: exit 1 on violation, 2 if unreadable

node scan.mjs --version                     # installed build + spec contract (offline), + upgrade line

node query.mjs show     .candor/report db.save 1   # a function's effects (match ladder)
node query.mjs where    .candor/report Net 1       # direct sources vs inheritors
node query.mjs callers  .candor/report db.save 1   # the blast radius (transitive callers)
node query.mjs map      .candor/report 1           # module → effects overview
node query.mjs whatif   .candor/report db.save Net policy  # pre-edit gate verdict (exit 1)
node query.mjs diff     .candor/report baseline 1  # per-function effect delta (exit 1 on a gain)

Staying current: check your installed version and upgrade — candor/AGENTS.md §2a. npx -y candor-ts --version prints the build, the spec, and the upgrade one-liner (offline; candor never phones home).

Function names are module-qualified with . segments (src.db.save), so policy scopes read naturally:

# .candor/policy
deny Net domain                       # the domain layer reaches no network, even through helpers
pure  parse                           # parsing is effect-free
allow Db in db  orders audit_log      # the db layer touches ONLY these tables
allow Net in billing api.stripe.com   # billing talks ONLY to Stripe
forbid domain -> infra                # the domain layer must not depend on infra

The report carries the four literal surfaces where a declaration makes them decidable — hosts at Net calls, tables at Db calls (SQL table positions, mirroring the Rust/JVM extractors exactly, plus TypeORM's @Entity("user") declarations read through the receiver's Repository<T> type argument), cmds at Exec, path-shaped paths at Fs — never from a runtime-computed value, propagated transitively, enforced by the allow rules above. On a real Nest app this makes table-level policy live: allow Db in article.service article comments flags the service reaching user and follows.

The classifier is curated (the same under-report-and-say-so posture as the other engines): the Node builtins (fs, net/http/tls, child_process, node:sqlite, process.env, the clock) plus a small npm tier (axios/got/node-fetch/undici/ws, pg/mysql2/mongodb/redis/knex, execa/cross-spawn, fs-extra/rimraf/glob, dotenv, winston/pino). An unlisted package contributes nothing — candor never guesses an effect.

MCP server — candor as agent ground truth

candor-ts-mcp exposes the read-only queries as an MCP server, so a coding agent can ask "if I change this, what's the runtime blast radius?" or "what reaches the network?" and get deterministic ground truth from a precomputed report — instead of burning tokens tracing the call graph by hand (the measured ~700–2000× token win on blast-radius questions).

// in an MCP client config — point it at a report you've already scanned
{ "command": "npx", "args": ["-y", "candor-ts-mcp"],
  "env": { "CANDOR_REPORT": ".candor/report.myPkg.scan" } }

Tools: candor_impact (backward blast radius), candor_reachable (what runs at runtime), candor_where (effect surface), candor_path (how an effect is reached), candor_callers, candor_show, candor_map, candor_whatif (pre-edit gate check). Each takes an optional report prefix (else $CANDOR_REPORT). The server is query-only — it never scans (the analyzer self-boundary, spec §7.12: an agent or a hook produces the report; the server reads it, Fs only). The query logic is the shared query-core.mjs, the same answers the CLI gives.

The live loopcandor-ts-watch keeps the report fresh as the agent edits, so the answers are about the current code, not a stale snapshot:

candor-ts-watch ./src --out .candor/report   # re-scans only when a tracked source actually changes

It tracks the project's sources by content hash and re-scans on a real change (a no-op save or an unrelated write does nothing), writing the same prefix the MCP server reads. So: agent edits → watcher refreshes the report → agent asks candor_impact and gets the post-edit answer. And it reports the edit-delta — not just that the report is fresh but what the edit did to the effect surface (re-scanned (1 changed: app.ts) — Δ f +Net), so the agent learns the consequence of its own change. v1 runs a full (sound) scan per change; the deeper perf optimisation — re-analysing only the changed file's subgraph instead of the whole project — is the staged next step (the content-hash gate is its first increment).

Trust contract (spec §4)

Anything candor-ts can't resolve is Unknown, never silently pure: a function-valued parameter or field being called, an any-typed callee, resolution landing on a type rather than a body.

An uncurated dependency can opt out of Unknown/silent-pure by declaring its effects in its package.json"candorEffects": ["Net"] (spec §5.1, the effect manifest). candor-ts reads it as the declared-not-verified tier: the package's calls classify to the declared set, and it stops being a κ-ledger blind spot. A name outside the §1 vocabulary voids the declaration loudly (a typo must not silently narrow a surface). And candor-ts-query gains <cur> <base> flags the supply-chain delta — the effects a surface gained between two reports. Real-world consequence, measured on rimraf (50 files, 55 functions analyzed): its DI-style fs injection means many functions honestly read Unknown — that's the contract working, not noise. The report says "can reach", never "does"; an absent literal is never a claim of absence.

Cross-engine consistency — machine-checked

candor-ts runs live in the spec's conformance CI as the third engine in three differentials: the effect-set oracle (20 shared cases), the §6.2 policy-grammar battery (including allow Db), and the §3.1 query-shape and match-ladder checks — all three engines must answer identically, on every push to the spec.

What the analysis core implements (and where the spec told it how)

| Piece | Spec source | |---|---| | Resolve every call via the compiler API (getResolvedSignature), never syntax | CLASSIFIER §1 | | κ classifies the resolved target's module (node:fs→Fs, node:net→Net, …) | CLASSIFIER §2, TS notes | | process.env property read → Env; Date.now → Clock | SPEC §1 | | Local edges (cross-file) + least-fixpoint propagation | SEMANTICS §5a | | Closure bodies attribute to the nearest enclosing function | SEMANTICS §2 | | A call resolving to a type (function-typed field/param) → Unknown, never silent-pure | SPEC §4 | | Unmatched external calls contribute nothing (curated-κ caveat) | SEMANTICS §8 C1 | | The literal surfaces hosts/cmds/paths/tables, literal-read only | SPEC §2 | | { candor: { version, toolchain, spec: "0.5" }, functions } envelope; pure fns omitted | SPEC §2/§2.1 | | Call-graph sidecar with every analyzed function a key | SPEC §2.2 | | The gate: AS-EFF-006 / 008 / 009, loud on an unreadable policy | SPEC §6.2 |

Origin: the derivability proof

This engine began as a deliberately minimal single-file slice written from the spec documents alone (SPEC.md, SEMANTICS.md, CLASSIFIER.md) — without consulting the Rust or JVM sources — to answer executably: is the spec enough to derive a new-language implementation? Yes — 20/20 on the shared oracle. That clean-room claim is frozen at commit a29b152; everything since (multi-file projects, the query surface, the gate, the literal surfaces) is spec-implemented but post-hoc, and its guarantee is the conformance differential above, not clean-room provenance. The one engine-fix the original derivation needed (a call landing on a function-type declaration read as pure until §4 was applied to it) remains the proof point: the fix was "do what §4 says", not "go read the Rust source".

Status

Young product (0.1.x): the analysis core, the gate, and the query surface are real, behaviorally tested (node test.mjs), soundness-fuzzed with verified teeth (node fuzz.mjs — spec §7.13: generated effect chains through every encoded call form, any silent-pure = red), and conformance-held. The npm classifier tier is deliberately curated and will keep growing case-by-case. Entry points (Nest/Next populations), unknownWhy origins, reachable, cross-package inheritance (CANDOR_DEPS + the spec §2 hash, version-trusted per §2.1), and --allow-js are all in. On npm: npx -y candor-ts <dir>.

Development

No build step — the engine runs on Node directly.

npm install
npm test            # the full CI gate: lint + unit (node:test) + behavioural + MCP + watch + the
                    # fabrication probe + the §7.13 soundness fuzzer
npm run test:unit   # just the native unit tests — the query algebra + policy DSL + the scan-core
                    # classifier/literal leaves (query-core / policy / scan-core)
npm run lint        # eslint (the recommended ruleset; the CI lint gate)
node scan.mjs <dir | file.ts | tsconfig.json> --out .candor/report   # scan a project

The pure cores are factored into importable modules — query-core.mjs (the §3.1 queries), policy.mjs (the §6.2 DSL + literal matchers), and scan-core.mjs (the κ classifier + the SQL/ command/host extractors) — so they're unit-tested directly; the TS-compiler-driven walk stays in scan.mjs.