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

psytools

v0.5.2

Published

Zero-dependency toolkit for psychological assessments: data models, response validation, scoring, and predefined inventories (PHQ-9, GAD-7, DASS-21, WHO-5, ASRS, AQ-10, AUDIT, Mini-IPIP) in multiple languages.

Readme

psytools

Give your app a validated psychological questionnaire — rendered, collected, and scored — in a few lines:

import { loadInventory } from "psytools";

const phq9 = await loadInventory("phq9");              // Patient Health Questionnaire-9
phq9.localize("en");                                   // render-ready questions & options for your UI

const response = phq9.createResponse();
response.answerAll([2, 1, 3, 1, 0, 1, 2, 0, 0]).submit();

phq9.evaluate(response);
// { score: 10, band: { id: "moderate", label: { en: "Moderate depression", ... } }, ... }

That's a complete depression screening: standardized items, answer validation, published severity cutoffs, and clinical flags (e.g. PHQ-9's self-harm item) — with zero dependencies.

Why psytools

  • Batteries included — eight instruments (PHQ-9, GAD-7, DASS-21, WHO-5, ASRS, AQ-10, AUDIT, Mini-IPIP) ship ready to use in English, Turkish, German, Chinese, and Spanish, with published scoring rules and citations.
  • Your tests too — therapists and researchers can define their own instruments as one plain JSON object; psytools validates, localizes, and scores them the same way.
  • Everything is plain JSON — assessments and responses stringify()/parse() losslessly, so definitions live in your database and travel between backend and frontend. Scoring rules are data, not code, and survive the round trip.
  • Safe by default — every answer is validated against the option scale, incomplete responses can't be scored accidentally, and submitted responses are immutable.
  • Data minimization by default — base definitions carry English only; other languages ship as per-inventory locale packs that are lazily imported (and only then enter memory or your bundle) when you request them.
  • Runs anywhere — zero runtime dependencies; Node (>= 18), browsers, and edge runtimes; ESM + CommonJS with full TypeScript declarations.

Install

npm install psytools

Walkthrough

import { loadInventory } from "psytools";

// Locales are lazy: only the requested packs are loaded.
const phq9 = await loadInventory("phq9", { locales: ["tr"] });

// 1. Render — get a flat, single-locale view for your UI.
const view = phq9.localize("tr");
// { id, locale, title, instructions, questions: [{ id, index, text, options: [{ value, label }] }] }

// 2. Collect answers — one by one or all at once.
const response = phq9.createResponse({ respondentId: "client-42" });
response.answer("phq9-1", 2);           // validated immediately
response.answerAll([2, 1, 3, 1, 0, 1, 2, 0, 0]); // or all at once (question order)
response.submit();                       // validates completeness, freezes, timestamps

// Responses track their lifecycle as `response.status`:
// "empty" -> "in-progress" -> "complete" -> "submitted"

// 3. Evaluate.
const result = phq9.evaluate(response);
// {
//   kind: "scale", score: 10, min: 0, max: 27,
//   band: { id: "moderate", label: { en: "Moderate depression", tr: ..., de: ... } },
//   flags: []   // phq9-9 > 0 would raise the "suicidality" flag here
// }

// evaluate() refuses responses that are not complete/submitted;
// score partial data explicitly:
phq9.evaluate(inProgressResponse, { allowIncomplete: true });

// 4. Persist — both sides are plain JSON.
db.save(response.stringify());           // response -> DB
db.save(phq9.stringify());               // definition -> DB

Restore later and continue where you left off:

import { Assessment, AssessmentResponse } from "psytools";

const assessment = Assessment.parse(jsonFromDb);            // validated on parse
const response = AssessmentResponse.parse(assessment, rowFromDb); // answers re-validated

Predefined inventories

| Id | Instrument | Items | Scoring | Locales | | --- | --- | --- | --- | --- | | phq9 | Patient Health Questionnaire-9 (depression) | 9 | Sum 0–27, 5 severity bands, item-9 suicidality flag | en, tr, de, zh, es | | gad7 | Generalized Anxiety Disorder-7 | 7 | Sum 0–21, 4 severity bands | en, tr, de, zh, es | | dass21 | Depression Anxiety Stress Scales (short form) | 21 | 3 subscales × 7 items, sums doubled, banded per subscale | en, tr, de, zh, es | | who5 | WHO-5 Well-Being Index | 5 | Sum × 4 → 0–100 (higher is better), 3 well-being bands | en, tr, de, zh, es | | asrs6 | Adult ADHD Self-Report Scale (ASRS-v1.1) screener | 6 | Count of screen-positive items (per-item thresholds), ≥4 positive | en, tr, de, zh, es | | aq10 | Autism Spectrum Quotient (AQ-10, adult) | 10 | 1 point per item in trait direction (agree/disagree), ≥6 refer | en, tr, de, zh, es | | audit | Alcohol Use Disorders Identification Test (WHO) | 10 | Sum 0–40 with per-question point systems, 4 WHO risk zones | en, tr, de, zh, es | | mini-ipip | Mini-IPIP Big Five personality scale | 20 | 5 trait subscales (4–20 each), reverse-keyed items, no cutoffs | en, tr, de, zh, es |

import { loadInventory, inventories, dass21 } from "psytools";

loadInventory("dass21");   // ready-to-use Assessment instance
inventories;               // { phq9, gad7, dass21 } raw definitions
dass21;                    // a plain AssessmentDefinition you can clone and adapt

DASS-21 evaluates to a multi-scale result:

const result = assessment.evaluate(response);
// { kind: "multiscale", scales: [
//     { id: "depression", score: 14, min: 0, max: 42, band: { id: "moderate", ... }, label: {...} },
//     { id: "anxiety", ... }, { id: "stress", ... }
//   ], flags: [] }

Defining your own assessment

An assessment is one plain object — questions, an enumerated option scale, and optional declarative scoring:

import { Assessment, type AssessmentDefinition } from "psytools";

const definition: AssessmentDefinition = {
  id: "my-clinic/sleep-check",
  version: "1.0.0",
  title: { en: "Sleep Check", tr: "Uyku Kontrolü" },
  instructions: { en: "Over the last week...", tr: "Geçen hafta boyunca..." },
  defaultLocale: "en",
  options: [
    { value: 0, label: { en: "Never", tr: "Hiçbir zaman" } },
    { value: 1, label: { en: "Sometimes", tr: "Bazen" } },
    { value: 2, label: { en: "Often", tr: "Sık sık" } },
  ],
  questions: [
    { id: "s1", text: { en: "I had trouble falling asleep", tr: "Uykuya dalmakta zorlandım" } },
    { id: "s2", text: { en: "I woke up rested", tr: "Dinlenmiş uyandım" }, reverseScored: true },
    { id: "s3", text: { en: "Anything else? (optional)", tr: "Başka bir şey? (isteğe bağlı)" }, optional: true },
  ],
  scoring: {
    kind: "sum",
    bands: [
      { id: "ok", min: 0, max: 2, label: { en: "No concern", tr: "Sorun yok" } },
      { id: "follow-up", min: 3, max: 6, label: { en: "Follow up", tr: "Takip edilmeli" } },
    ],
  },
};

const assessment = new Assessment(definition); // throws PsytoolsError on malformed definitions

Questions may also carry their own options to override the default scale, and validateDefinition(input) checks user-created definitions without throwing (returns { valid, issues }) — useful before persisting a therapist-authored test.

Non-JavaScript backends can validate stored definitions against the published JSON Schema (shipped in the npm package under schema/).

Data minimization

Minimization is the default, at every layer:

  • Memory / runtime: base definitions are English-only. loadInventory(id, { locales }) dynamically imports just the requested locale packs and merges them — nothing else is ever loaded. availableLocales(id) lists what exists.

  • Bundles: import a single inventory via psytools/inventories/<id> (~2 kB gzipped) and, for static bundling, a specific pack via psytools/locales/<locale>/<id>, merged with applyLocale:

    import { phq9 } from "psytools/inventories/phq9";        // en base
    import tr from "psytools/locales/tr/phq9";               // one language pack
    import { Assessment, applyLocale } from "psytools";
    
    const assessment = new Assessment(applyLocale(phq9, tr));

    Named root imports also tree-shake (sideEffects: false).

  • Payloads: a loaded definition contains exactly the locales you asked for, so res.json(assessment.definition) is already minimal. To slim an existing multi-locale definition (e.g. a stored custom test), use pickLocales(definition, [userLocale]).

Merged definitions remain one self-contained JSON object — locale loading never breaks the stringify → DB → parse round trip.

Local translation overrides

The bundled translations are unverified AI output (see Translation provenance below) — you may want to reword a specific question, option label, or instruction locally without forking the inventory. loadInventory accepts overrides: one or more partial locale packs, applied after the built-in packs, later-wins:

const phq9 = await loadInventory("phq9", {
  locales: ["tr"],
  overrides: {
    id: "phq9",
    locale: "tr",
    // Only the bits you want to change — everything else keeps the
    // bundled tr text (or falls back to English if tr was never loaded).
    questions: {
      "phq9-3": { text: "Uykuya dalmakta veya uykuyu sürdürmekte güçlük" },
    },
  },
});

An override pack has the same shape as a bundled InventoryLocalePack (see applyLocale above), but every field except id/locale/questions is optional, and questions only needs entries for what you're changing. Pass an array to apply several overrides in order; option-count mismatches and unknown question ids still throw invalid_argument, same as a bundled pack. This is a local, in-memory concern — psytools doesn't track override provenance or verification status; that's still on you and your own docs.

Missing answers

Declarative scoring takes an optional missing-data policy governing how incomplete answer sets are scored (they reach the scorer via raw answer maps or allowIncomplete):

scoring: {
  kind: "sum",
  missing: { strategy: "prorate", minAnswered: 7 }, // or "ignore" (default) / "require-complete"
  ...
}

ignore sums what is answered (partial totals understate severity — use deliberately); prorate scales the raw score up to the full item count (rounded) and refuses to score below minAnswered; require-complete throws unless every contributing non-optional item is answered. For subscale scoring the policy applies to each subscale independently.

The bundled inventories ship with policies (PHQ-9/GAD-7/DASS-21/Mini-IPIP prorate with thresholds, WHO-5/ASRS/AQ-10 require-complete, AUDIT ignores to match its skip logic) — see SOURCES.md for the rationale and how to override.

Custom evaluation

Declarative scoring is optional. Pass an evaluator for anything beyond sums and subscales — it can return a single scale, multiple scales, a category, or arbitrary data:

import type { Evaluator } from "psytools";

const attachmentStyle: Evaluator = (definition, answers) => {
  const avoidance = answers["a1"]! + answers["a2"]!;
  const anxiety = answers["b1"]! + answers["b2"]!;
  return {
    kind: "categorical",
    category: avoidance > 3 ? (anxiety > 3 ? "fearful" : "dismissive") : anxiety > 3 ? "preoccupied" : "secure",
  };
};

assessment.evaluate(response, { evaluator: attachmentStyle });

Result shapes: scale (single score + band + flags), multiscale (named subscales), categorical, and custom (escape hatch).

Tracking progress over time

Responses carry respondentId, startedAt/submittedAt timestamps, the assessment id/version, and free-form meta — store the serialized responses and evaluate them in bulk:

const history = rows
  .map((row) => AssessmentResponse.parse(phq9, row))
  .map((r) => ({ at: r.submittedAt, score: phq9.evaluate(r).score }));

API overview

| Export | Purpose | | --- | --- | | Assessment | Wraps a definition: validation, localize(), createResponse(), evaluate(), stringify()/parse() | | AssessmentResponse | answer(), answerAll(), clearAnswer(), status, progress(), validate(), submit(), stringify()/parse() | | evaluate(definition, answers, { evaluator? }) | Standalone scoring (also available as Assessment#evaluate) | | validateDefinition(input) | Non-throwing structural validation of definitions | | loadInventory(id, { locales?, overrides? }) / inventories | Predefined instruments (async; English-only base definitions also exported by name); overrides applies local partial locale packs after the built-in ones | | applyLocale(definition, pack) / availableLocales(id) / localePacks | Lazy per-inventory locale packs | | localizeText(text, locale, fallback?) / collectLocales(texts) / pickLocales(definition, locales) | Locale helpers | | PsytoolsError | All errors carry a code typed as PsytoolsErrorCode (e.g. invalid_value, already_submitted) |

Development

npm install
npm test        # node:test on TypeScript sources (no test framework needed)
npm run build   # ESM + CJS + type declarations into dist/

Important notes

  • Not a diagnostic tool. Scores, severity bands, and flags implement the published scoring rules of screening instruments; they are not diagnoses and do not replace clinical judgment.
  • Crisis-relevant answers. PHQ-9 item 9 raises a suicidality flag — applications should surface this to a responsible clinician and provide crisis resources to respondents.
  • Instrument licensing. SOURCES.md flags every bundled instrument as Free (PHQ-9, GAD-7, DASS-21, WHO-5 — usable without permission, including commercially) or Free with conditions (ASRS v1.1, AQ-10 — free for clinical/research use; verify with the rights holder before embedding in commercial products). Each inventory's meta.licenseFlag carries the same flag programmatically.
  • Translation provenance. English texts are reproduced from the original instruments. Turkish, German, Simplified Chinese, and Spanish texts are AI translations (Anthropic Claude, model claude-fable-5) of the original English — official translations were not consulted and the texts are unverified. This status is documentation-only: psytools does not track or expose per-pack verification programmatically (no verified field, no gating option, no status API) — read SOURCES.md and decide for yourself whether the bundled wording is fit for your use case. If it isn't for a specific item, use local overrides rather than forking the package. Obtain and compare the official version for your language before clinical use. Corrections: [email protected].

License

MIT