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

rubric-tool

v0.2.1

Published

Rubric — a governance checker for JSON and YAML documents that tells you what it found, and never passes silently

Readme

Rubric

A governance checker for JSON and YAML that tells you what it found — and never passes silently.

openapi.yaml
  error         16:7  operation-summary
    $.paths['/pets'].post — `summary` is empty
    Every operation needs a summary. It is what shows in the endpoint list.
  error        14:11  error-responses-described
    $.paths['/pets'].get.responses['404'] — `description` is missing
    An error response needs a description saying when it happens.

1 rule matched nothing
  webhooks-documented
    the selector found no nodes. Either this document has none, or the rule
    is wrong. If the rule only applies to some documents, add a `when`
    saying which.

2 errors, 1 broken rule

Status: early. The engine, the ruleset language and both CLIs work. It has not been used in anger by anyone, and 1.0 is not close.

Install

cargo install rubric-tool     # native binary
npx rubric-tool --help        # WASM, no install

Both are the same code. render() lives in the Rust core, so the two front ends produce byte-identical output and the same exit codes.

Use

rubric --rules rubric.yaml openapi.yaml
rubric --rules rubric.yaml specs/*.yaml --json
rubric --rules rubric.yaml --project rubric/project.yaml specs/**/*.yaml

| exit | | |---|---| | 0 | nothing failing — which can include rules that did not apply, and waived findings, all of them listed | | 1 | an error, a broken rule, a document that does not parse, or a run that failed as a whole: a ruleset that applied to none of the files, or rules limited to profiles with no --project | | 2 | the ruleset or the project file does not load |

--json prints one document for the whole run:

{
  "files": [
    {
      "file": "openapi.yaml",
      "notApplicable": null,
      "findings": [],
      "brokenRules": [],
      "notApplicableRules": [],
      "waived": [],
      "expiredWaivers": [],
      "counts": { "errors": 0, "warnings": 0, "broken": 0, "notApplicable": 0, "waived": 0 }
    }
  ],
  "notices": [],
  "failed": false
}

notApplicable is the reason the ruleset's applies-to excluded the document, or null. A document that does not parse appears as { "file", "error", "line" }. notices are what is true of the run rather than one file, each with failing and message. What changed in this shape between versions is in CHANGELOG.md.

A ruleset

rules:
  - id: operation-summary
    select: $.paths.*[[email protected]]
    require: summary
    message: Every operation needs a summary. It is what shows in the endpoint list.

  - id: path-segments-kebab
    select: $.paths.*.@property
    casing:
      style: kebab
      separator: { char: "/", allowLeading: true }
    message: Path segments are kebab-case. /petStore should be /pet-store.

  - id: operation-ids-unique
    select: $.paths.*[[email protected]]
    unique: operationId
    message: Two operations share an operationId. Generated clients will collide.

Fourteen assertions — require present absent match not-match casing one-of length exactly-one-of at-least-one-of unique sorted referenced none — documented in the ruleset spec. Selectors are RFC 9535 JSONPath plus keys and ancestors, in the selector spec.

Nothing in it is OpenAPI-specific. The engine reads JSON and YAML; that last example above would work unchanged on a Kubernetes manifest or a CI config.

Rules can also depend on what a document cannot say about itself. A project file assigns profiles by path — public: [apis/public/**] — and a rule with profiles: [public] applies only to those documents. A misspelt profile is a broken rule, and a run that forgot --project fails rather than quietly skipping every profiled rule. See the spec.

The project file can also hold waivers: a rule's findings accepted in some documents, with a required reason and an optional until date. A waived finding is still reported, marked with its reason; it just does not fail the run. An expired waiver's findings come back, and one that no longer waives anything is noted so it can be removed. until ends the waiver, never the rule: it is for a grace period, such as documents written before a rule existed, which must comply by a date. See the spec.

Three decisions worth knowing before you use it

A rule that matches nothing is a broken rule. Every comparable tool treats an unmatched selector as silence, which makes a typo indistinguishable from a clean document — and that is how a style guide quietly stops guarding anything. Here it is reported, counted separately, and fails the build. When a rule only applies to some documents, a when says which: a document without webhooks gets the webhook rule reported as not applicable, while one with webhooks and a mistyped selector still gets it reported as broken. See the spec.

message is required, and it says what to do. Not "expected value to be truthy" — that describes a failed assertion, not a problem, and it is why people scroll past linter output. Write the sentence you would say to whoever has to fix it.

require accepts false and 0. They are answers. deprecated: false is a documented fact and retries: 0 is a configured value. Other linters use JavaScript truthiness here and reject both — emptiness is a property of JSON values, truthiness is a property of JavaScript, and it has no business in a vocabulary about documents.

Rules to start from

rulesets/openapi.yaml is a starter ruleset for OpenAPI 3.x — seventeen rules covering the gaps a reader would actually notice: undescribed responses and parameters, missing operationIds, untagged operations, naming that generated clients will mangle.

rubric --rules rulesets/openapi.yaml openapi.yaml

Copy it and edit it. There is no extends, so the copy is yours — and a ruleset that cannot be silently changed underneath you is the point, not an omission. CI holds it to both halves of being useful: a clean document produces nothing and no broken rules, and a bad one produces findings. The second is what stops the first being satisfied by a ruleset that asserts nothing.

It declares applies-to OpenAPI 3, so run against another format or a Swagger 2.0 document it reports that document as not applicable, rather than handing it findings from rules it was never meant to satisfy.

Coming from Spectral

Every Spectral core function maps onto one assertion, so importing a ruleset is mechanical: given → select, then.function → the assertion key, then.functionOptions → its value, then.field appended to the selector. The ruleset spec has the table and the two translations that are not one-to-one.

The differences you will notice first: one level instead of three, a message that has to say something, and rules that shout when they match nothing.

As a library

use rubric::{check::check, document::Document, render::{render, Style}, rule::Ruleset};

let doc = Document::parse(&yaml).expect("document does not parse");
let rules = Ruleset::parse(&ruleset).expect("ruleset does not load");
let report = check(&doc, &rules);

print!("{}", render(&report, "openapi.yaml", Style::Human { color: false }));

examples/readme.rs is that snippet as a compiled example — it produces the output at the top of this file, so cargo run --example readme fails if either drifts.

Embedding it in another Rust program — Docy links it from src-tauri this way:

rubric-tool = { version = "0.2", default-features = false, features = ["serde"] }

default-features = false drops clap, which only the CLI needs; serde puts Serialize on Report and everything in it, so findings cross a Tauri command without being re-modelled on the way.

const { check_document } = require('rubric-tool')
process.stdout.write(check_document(document, ruleset, 'openapi.yaml', 'human'))

check knows only the document. check::check_with also takes a Context — the project file, the document's path, and today's date for waivers — and run::check_run checks several documents together and adds the run-level notices. From JavaScript, check_files is the whole run, and is what the rubric command itself calls.

How it is built

One Rust crate is the only place any logic lives. Two implementations of a governance tool is precisely the failure such a tool exists to prevent — a ruleset that passes in CI and fails in an editor is worse than no ruleset.

| | | |---|---| | selector | RFC 9535 JSONPath, plus .@property, @parent, @grandparent and ~= | | document | one parse producing both the value tree and a path → span index | | rule | the ruleset language | | check | the fourteen assertions, and the guards that decide whether a rule applies | | project | the project file: profiles, and the path patterns that assign them | | waiver | waivers, and the dates they are checked against | | run | several documents as one run, and what is true of the run as a whole | | render | what a person reads — in the core, so no front end can drift |

The npm package is the same crate compiled to WASM (106 KB gzipped), not a reimplementation. The core does no I/O — it takes strings and returns a report — so JavaScript reads the files and no WASI or filesystem shim is needed.

Selectors are parsed, never evaluated. The obvious implementation is jsonpath-plus, which runs filters through a JavaScript evaluator; that is what CVE-2024-21534 was, and a ruleset may be imported from elsewhere. A selector here parses into a structure that can only describe a selection, and there is deliberately no custom-function mechanism at all.

Regular expressions use regex-lite: linear time, so a hostile pattern cannot hang the process, and small enough to ship as WASM. The cost is ASCII-only character classes.

Development

cargo test                    # and --features serde, for the embedder's types
cargo clippy --all-targets
cargo run --example readme    # the snippet above, diffed against this README
npm run build                 # wasm-pack into npm/pkg, then check the tarball
tools/check-parity.sh         # both front ends, byte for byte
tools/check-rulesets.sh       # the starter ruleset: clean, loud, and silent on other formats
node tools/check-versions.mjs # Cargo.toml, npm/package.json and the tag agree

Changes are recorded in CHANGELOG.md, with anything breaking listed as such.

CI runs all of that on every branch, and a v* tag publishes to crates.io and npm from one commit — npm gets the artifact the parity check ran against rather than a second build made the same way.

Tests are checked by mutation, not just by passing: break the code on purpose and confirm a test notices. Several in here exist because a first version did not.

The build script ends by refusing a package tarball with no .wasm in it. wasm-pack writes a .gitignore containing * into its output directory, and npm reads a nested .gitignore as an exclusion list — so files: ["pkg"] publishes three files, a valid package, and an npx rubric-tool that cannot find its own binary. It is the failure this tool is named for, in its own release process.

Licence

Apache-2.0.