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

ragproof

v0.1.0

Published

Self-correcting RAG answers and CI eval gates that block the merge when hallucination rate crosses your threshold

Readme

ragproof

Your RAG, put to proof.

CI npm license

A self-correcting answer loop and a CI eval gate that blocks the merge when your RAG's hallucination rate crosses a threshold you set.

Why

RAG pipelines in production hallucinate silently. Nobody measures the hallucination rate continuously — the pipeline gets "reviewed" once and shipped, and every change after that (prompt tweak, chunking strategy, model swap) can quietly degrade answer quality with no test catching it.

ragproof gives you both halves of a fix, built on the same primitive:

  • At runtime, a corrective loop checks that every answer is actually grounded in the chunks your retriever returned. If it isn't, the loop reformulates the query and retries, and if it still can't find support it returns an honest "insufficient evidence" answer instead of inventing one.
  • In CI, an eval harness runs your pipeline against a golden dataset on every push and fails the job — blocking the merge — when the hallucination rate (or any other configured metric) crosses your threshold.

Built for teams with a RAG in production who want measurable guarantees instead of vibes, and for consultants who need to demonstrate quality to a client rather than assert it.

What it does

Both the runtime loop and the CI eval consume the same core primitive: given a question, the chunks retrieved, and the generated answer, an LLM judge extracts every factual claim in the answer and classifies each one as supported, unsupported, or contradicted by the chunks. One engine, two faces. If the judge extracts zero claims from a non-empty answer, the overall verdict is no_claims with score 0; the loop treats it as unverifiable and never as a grounded success. supported and contradicted claims must cite a non-empty id from the supplied chunks; unsupported claims may omit evidence and, for backward compatibility, may include it without id validation.

ragproof does not ship a retrieval or generation stack. You bring retrieve(query) and generate(query, chunks) from whatever RAG you already have; ragproof orchestrates retrieve → generate → verdict → (if unsupported) reformulate the query and re-retrieve, accumulating chunks across attempts, up to maxAttempts (default 2).

Quickstart

npx ragproof init

This creates three files in the current directory:

  • .ragproofrc.json — judge, pipeline, loop, and eval configuration
  • golden.example.jsonl — a 3-item example dataset
  • pipeline.mjs — a starter pipeline (extractive keyword RAG) that works out of the box against the example dataset

The starter pipeline exists so your first ragproof eval runs end to end. Replace its retrieve and generate with your own retriever and LLM — see examples/pipeline.example.mjs for a commented template.

Set your judge's API key (the default config uses the anthropic adapter):

export ANTHROPIC_API_KEY=sk-ant-...

Then validate the setup and run the eval:

npx ragproof doctor
npx ragproof eval

doctor checks the config file, the dataset, the pipeline module, and judge connectivity (skip the network check with --offline). eval runs every dataset item through the corrective loop, prints an aggregate report, and exits non-zero if any configured gate fails.

The corrective loop, programmatically

runCorrectiveLoop is also a library export — use it directly at request time in your own RAG service instead of (or in addition to) running it via the CLI's eval command:

import { AnthropicAdapter, runCorrectiveLoop } from "ragproof";

const judge = new AnthropicAdapter({ model: "claude-sonnet-5" });

const result = await runCorrectiveLoop({
  question: "What is the capital of Spain?",
  judge,
  retrieve: async (query) => myRetriever.search(query),
  generate: async (query, chunks) => myLlm.answer(query, chunks),
  maxAttempts: 2,       // default: 2
  minGroundedness: 1,   // default: 1 (accept only fully supported answers)
});

if (result.insufficientEvidence) {
  // result.answer is the deterministic honest fallback, listing
  // every unsupported/contradicted claim from the last attempt
} else {
  // result.answer is grounded in result.attempts.at(-1).chunks
}

retrieve and generate failures are wrapped in LoopExecutionError with operation ("retrieve" or "generate") and attempt so you can tell which side of your pipeline broke and on which try.

Setting minGroundedness < 1 can accept an answer whose overall verdict is unsupported when its score meets the threshold. Such a result still has insufficientEvidence: false, but partiallyGrounded: true distinguishes it from a clean fully-supported answer. Accepting partially-grounded answers trades higher answer rate for increased hallucination risk.

Config reference (.ragproofrc.json)

The full schema, enforced with zod (src/cli/config.ts):

{
  "judge": {
    "adapter": "anthropic",       // "anthropic" | "openai-compatible"
    "model": "claude-sonnet-5",   // optional; adapter-specific default
    "baseUrl": "https://...",     // required when adapter is "openai-compatible"
    "apiKeyEnv": "ANTHROPIC_API_KEY" // optional; env var holding the key
  },
  "pipeline": {
    "module": "./pipeline.mjs"    // required; ESM module exporting retrieve + generate
  },
  "loop": {                       // optional
    "maxAttempts": 2,             // positive integer, default 2
    "minGroundedness": 1          // (0, 1], default 1
  },
  "eval": {                       // optional
    "dataset": "./golden.example.jsonl",
    "concurrency": 4,             // positive integer, default 4
    "gates": ["hallucination<=0.05"]
  }
}

Paths (pipeline.module, eval.dataset) resolve relative to the config file's directory, not the current working directory.

Judge adapters

  • anthropic — calls the Anthropic Messages API directly.
  • openai-compatible — calls POST {baseUrl}/chat/completions; works against OpenAI, OpenRouter, Moonshot, Ollama, or anything else that speaks the OpenAI chat-completions shape. baseUrl is required.

API keys are read from the env var named by apiKeyEnv (default ANTHROPIC_API_KEY / OPENAI_API_KEY), or from a file whose path is in <apiKeyEnv>_FILE (Docker-secrets style) — never from the command line, and never logged.

Eval dataset format

One JSON object per line (.jsonl):

{"id": "spain-capital", "question": "What is the capital of Spain?", "expected_facts": ["Madrid is the capital of Spain."], "reference_answer": "Madrid is the capital of Spain."}

Only question is required. id defaults to the item's 1-based position among non-empty lines; blank lines are skipped and do not increment it. expected_facts enables the retrieval_hit metric for that item; reference_answer is carried through for your own reporting but is not currently consumed by any metric. An empty or all-blank dataset is rejected because an eval with zero items cannot produce trustworthy measurements.

Metrics

Computed per eval run over the dataset items (src/eval/metrics.ts):

| Metric | Meaning | Denominator | | --- | --- | --- | | hallucinationRate | Share of answered/insufficient-evidence items with at least one unsupported or contradicted claim | items with a verdict (excludes errors) | | insufficientEvidenceRate | Share of items where the loop gave up and returned the honest fallback | items with a verdict | | errorRate | Share of items where retrieve/generate threw | all dataset items | | meanGroundedness | Average of each item's groundedness score (supported claims / total claims) | non-error items | | retrievalHitRate | Average fraction of expected_facts found (normalized substring match) in the final attempt's chunks | non-error items that declared expected_facts | | latencyP50Ms / latencyP95Ms | Nearest-rank percentiles of per-item wall time | non-error items |

Any metric whose denominator is empty reports 0 in the aggregate report for stable serialization. A gate over that metric fails closed with reason: "no data"; the reported zero is never treated as a passing measurement.

Gates

A gate is <metric><op><threshold>, e.g. hallucination<=0.05, retrieval_hit>=0.8, latency_p95<2000.

Valid metrics: hallucination, insufficient_evidence, error, groundedness, retrieval_hit, latency_p95. Valid operators: <=, >=, <, >.

Configure gates in .ragproofrc.json (eval.gates, an array) or override them per invocation with --gate (repeatable — replaces the configured list entirely, doesn't merge with it):

npx ragproof eval --gate "hallucination<=0.05" --gate "error<=0"

By default, any item-level pipeline error makes ragproof eval exit 2 before gates are evaluated because the run is not trustworthy. The explicit --allow-item-errors flag skips this safety check and evaluates gates over the remaining data; this is unsafe and intended only for workflows that knowingly accept partial eval runs.

ragproof eval exits 0 only if every gate passes, 1 if any gate fails, 2 on a setup/execution error (bad config, unreadable dataset, pipeline import failure, empty dataset, or non-allowed item errors).

CI

- run: npx ragproof eval --gate "hallucination<=0.05"
  env:
    ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}

When GITHUB_STEP_SUMMARY is set (true on GitHub Actions), ragproof eval also appends a Markdown table of metrics and gate results to the job summary automatically — no extra flag needed. See examples/github-action.yml for a complete workflow, and docs/failure-modes.md for what each non-zero exit code means and how to debug it.

Security considerations

Treat .ragproofrc.json and the configured pipeline.module as fully trusted code and configuration, at the same trust level as your CI secrets. The pipeline module is loaded with dynamic import() and executes with the complete environment available to the ragproof process. The combination of judge.apiKeyEnv and judge.baseUrl determines which environment secret is sent to which host. An attacker who can edit either the config or the pipeline module can therefore exfiltrate a selected secret to a host they control.

Never run ragproof eval on a pull request from an untrusted fork while real secrets such as ANTHROPIC_API_KEY are available to the job. Use GitHub environment protection rules and required reviewers for workflows that make real secrets available, especially workflows triggered by pull_request_target or similar events that can expose secrets to fork PRs.

Retrieved evidence is also untrusted input to the LLM judge. ragproof wraps questions, answers, and evidence chunks in explicit data-only delimiters and instructs the judge not to follow commands found inside them. This mitigates prompt injection but does not eliminate it: a judge can still misclassify a claim through ordinary model error or a sufficiently crafted injection. Treat the reported hallucination rate as a strong signal, not a formal security guarantee.

Honest limitations

  • The judge is an LLM. Every verdict is a probabilistic classification from another model, not a proof. A judge can misclassify a claim just like the model it's grading can hallucinate one. Treat the hallucination rate as a strong, continuously-checked signal — not a formal guarantee.
  • retrieval_hit is naive. It's normalized substring matching between expected_facts and the retrieved chunk text, not semantic similarity. A correct paraphrase that doesn't share the literal wording will register as a miss.
  • No cost metric yet. Each eval item costs at least one judge call, plus one more per correction attempt the loop makes. Rough estimate: items × (1 judge call + up to (maxAttempts − 1) more if answers keep failing the groundedness check). There is no built-in token/dollar tracking — budget your own runs accordingly.
  • Module-mode only. The pipeline is loaded as an ESM module (import()), not spawned as an external command. If your RAG lives in another language or process, write a thin .mjs wrapper that calls out to it.
  • Every eval call spends money. ragproof eval invokes the judge for every dataset item (and again per correction attempt), so running it on every push has a real, ongoing API cost — size your dataset and CI frequency accordingly.

License

MIT © 2026 Nestor Martinez — see LICENSE.